From cbd96f301d87078b01c6852723bf0861bdfbca11 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 27 Oct 2016 17:33:34 +0100 Subject: [PATCH] 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 | 529 ++++++++++++------ .../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, 830 insertions(+), 252 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 557a13d2..a1eeb908 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 9701f29f..5802e260 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.RestDocumentationRequestBuild 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 class Payload { 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 class Payload { // 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 d54a7e6d..c5734d42 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.beneathP 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 class Payload { // 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 class Payload { // 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 2c014fc0..102426b9 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 82a30e34..bfeeb626 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.mockmvc.RestDocumentationRequestBuild 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 @@ import com.fasterxml.jackson.databind.ObjectMapper; @SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class) @WebAppConfiguration public class ApiDocumentation { - + @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @@ -116,7 +117,7 @@ public class ApiDocumentation { 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 class ApiDocumentation { 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 class ApiDocumentation { 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 class ApiDocumentation { 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 class ApiDocumentation { 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 f6345840..55b1e036 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.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.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 @@ import com.fasterxml.jackson.databind.ObjectMapper; @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 class ApiDocumentation { 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 class ApiDocumentation { 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 class ApiDocumentation { 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 class ApiDocumentation { 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 class ApiDocumentation { .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 class ApiDocumentation { 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 class ApiDocumentation { 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 class ApiDocumentation { tag.put("name", "REST"); ConstrainedFields fields = new ConstrainedFields(TagInput.class); - + this.mockMvc .perform(post("/tags") .contentType(MediaTypes.HAL_JSON) @@ -344,7 +345,7 @@ public class ApiDocumentation { 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 class ApiDocumentation { 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 a630cc38..8c83ab97 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 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { 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 d68bb68d..edc48443 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 f1fff9ce..1dc23a5a 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 @@ class JsonContentHandler implements ContentHandler { 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 @@ class JsonContentHandler implements ContentHandler { 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 dbaf6d88..dfbd5ff4 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 @@ final class JsonFieldProcessor { }); } + 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 @@ final class JsonFieldProcessor { @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 @@ final class JsonFieldProcessor { @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 @@ final class JsonFieldProcessor { 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 286cd185..3f31ef76 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 abstract class PayloadDocumentation { 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 abstract class PayloadDocumentation { * 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 abstract class PayloadDocumentation { * 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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { *

* 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 abstract class PayloadDocumentation { *

* 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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * 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 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, 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 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 abstract class PayloadDocumentation { * 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 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, 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 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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * 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 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, 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 @@ -391,6 +482,7 @@ public abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * 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 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, 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 @@ -422,6 +516,7 @@ public abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * {@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 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, 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 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 abstract class PayloadDocumentation { * {@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 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, 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 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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * 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 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, 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 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 abstract class PayloadDocumentation { * 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 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, 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 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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * 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 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, 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 part the part name * @param subsectionExtractor the subsection extractor @@ -686,6 +801,7 @@ public abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * 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 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, 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 part the part name * @param subsectionExtractor the subsection extractor @@ -717,6 +835,7 @@ public abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * 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 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, 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 part the part name * @param subsectionExtractor the subsection extractor @@ -797,6 +920,7 @@ public abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * 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 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, 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 part the part name * @param subsectionExtractor the subsection extractor @@ -831,6 +957,7 @@ public abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { *

* 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 abstract class PayloadDocumentation { *

* 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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { *

* 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 abstract class PayloadDocumentation { *

* 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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * 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 abstract class PayloadDocumentation { * 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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * 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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * 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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { * @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 abstract class PayloadDocumentation { 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 00000000..9d7756bf --- /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 2e8f99e7..f5a466ff 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 @@ package org.springframework.restdocs.payload; 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 javax.xml.xpath.XPathFactory; 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 @@ class XmlContentHandler implements ContentHandler { @Override public String getUndocumentedContent(List fieldDescriptors) { Document payload = readPayload(); + List matchedButNotRemoved = new ArrayList<>(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { NodeList matchingNodes; try { @@ -124,17 +127,50 @@ class XmlContentHandler implements ContentHandler { 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 395ca4df..145f6121 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 class JsonFieldProcessorTests { 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 class JsonFieldProcessorTests { 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 d6fd3998..80c9cdf1 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 class RequestFieldsSnippetFailureTests { @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 class RequestFieldsSnippetFailureTests { .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 d3ad4e25..9ad807f7 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.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.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -65,6 +66,19 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests { .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 class RequestFieldsSnippetTests extends AbstractSnippetTests { .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 class RequestFieldsSnippetTests extends AbstractSnippetTests { .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 00000000..5c9f1b6e --- /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 5429591f..26ea2c95 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.operation.preprocess.Preprocessors.re 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 class MockMvcRestDocumentationIntegrationTests { 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 d1089ecc..6d1a84e3 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.operation.preprocess.Preprocessors.re 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 class RestAssuredRestDocumentationIntegrationTests { 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);