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 d6048547..82d172a2 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 @@ -31,6 +31,7 @@ * extracted and removed. * * @author Andy Wilkinson + * @author Sharang Gupta * */ final class JsonFieldProcessor { @@ -72,6 +73,11 @@ public void foundMatch(Match match) { match.remove(); } + @Override + public void foundEmptyCollection(Match match) { + match.remove(); + } + }); } @@ -83,6 +89,11 @@ public void foundMatch(Match match) { match.removeSubsection(); } + @Override + public void foundEmptyCollection(Match match) { + match.removeSubsection(); + } + }); } @@ -107,6 +118,9 @@ private void handleCollectionPayload(Collection collection, MatchCallback mat if (context.isLeaf()) { matchCallback.foundMatch(new LeafCollectionMatch(collection, context.getParentMatch())); } + else if (collection.isEmpty()) { + matchCallback.foundEmptyCollection(new LeafCollectionMatch(collection, context.getParentMatch())); + } else { Iterator items = collection.iterator(); while (items.hasNext()) { @@ -369,6 +383,14 @@ private interface MatchCallback { default void absent() { } + /** + * Called when the path descends into a collection that has no entries. The + * collection is covered by the path but contains nothing that can be matched. + * @param match a match for the empty collection + */ + default void foundEmptyCollection(Match match) { + } + } private interface Match { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java index e421ab31..c5e5d7af 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java @@ -225,4 +225,13 @@ void describedMissingFieldThatIsChildOfOptionalObjectThatIsNullIsNotConsideredMi assertThat(missingFields.size()).isEqualTo(0); } + @Test + void describedFieldBeneathArrayThatIsSometimesEmptyDoesNotLeaveUndocumentedContent() { + List descriptors = Arrays.asList(new FieldDescriptor("count"), + new FieldDescriptor("items[].k1"), new FieldDescriptor("items[].k2[].name")); + String content = "{\"count\": 2, \"items\": [{\"k1\": \"v1\", \"k2\": []}, " + + "{\"k1\": \"v2\", \"k2\": [{\"name\": \"joe\"}, {\"name\": \"alice\"}]}]}"; + assertThat(new JsonContentHandler(content.getBytes(), descriptors).getUndocumentedContent()).isNull(); + } + } 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 866663f4..3d61e32f 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 @@ -315,6 +315,32 @@ void removeSubsectionRemovesArrayWithListEntries() { assertThat(payload.size()).isEqualTo(0); } + @SuppressWarnings("unchecked") + @Test + void removeFieldBeneathArrayThatIsSometimesEmpty() { + Map payload = new ObjectMapper() + .readValue("{\"a\": [{\"b\": []}, {\"b\": [{\"c\": \"charlie\"}, {\"c\": \"charlie\"}]}]}", Map.class); + this.fieldProcessor.remove("a[].b[].c", payload); + assertThat(payload.size()).isEqualTo(0); + } + + @SuppressWarnings("unchecked") + @Test + void removeFieldBeneathArrayThatIsAlwaysEmpty() { + Map payload = new ObjectMapper().readValue("{\"a\": [{\"b\": []}]}", Map.class); + this.fieldProcessor.remove("a[].b[].c", payload); + assertThat(payload.size()).isEqualTo(0); + } + + @SuppressWarnings("unchecked") + @Test + void removeSubsectionBeneathArrayThatIsSometimesEmpty() { + Map payload = new ObjectMapper() + .readValue("{\"a\": [{\"b\": []}, {\"b\": [{\"c\": {\"d\": \"delta\"}}]}]}", Map.class); + this.fieldProcessor.removeSubsection("a[].b[].c", payload); + assertThat(payload.size()).isEqualTo(0); + } + @Test void extractNestedEntryWithDotInKeys() { Map payload = new HashMap<>(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 2dd06097..4f34b73f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -384,6 +384,22 @@ void optionalFieldBeneathArrayThatIsSometimesAbsent(OperationBuilder operationBu .row("`a[].c`", "`Number`", "two")); } + @RenderedSnippetTest + void fieldBeneathArrayThatIsSometimesEmpty(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("count").description("one"), fieldWithPath("items[].k1").description("two"), + fieldWithPath("items[].k2[].name").description("three"))) + .document(operationBuilder.response() + .content("{\"count\": 2, \"items\": [{\"k1\": \"v1\", \"k2\": []}, " + + "{\"k1\": \"v2\", \"k2\": [{\"name\": \"joe\"}, {\"name\": \"alice\"}]}]}") + .build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`count`", "`Number`", "one") + .row("`items[].k1`", "`String`", "two") + .row("`items[].k2[].name`", "`String`", "three")); + } + @RenderedSnippetTest void typeDeterminationDoesNotSetTypeOnDescriptor(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException {