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);