Skip to content

Commit 93a43df

Browse files
committed
Consider optionality when finding uncommon fields in subsection extraction
Fixes gh-573
1 parent 3f66b06 commit 93a43df

File tree

6 files changed

+104
-18
lines changed

6 files changed

+104
-18
lines changed

spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ protected Map<String, Object> createModel(Operation operation) {
151151
}
152152
MediaType contentType = getContentType(operation);
153153
if (this.subsectionExtractor != null) {
154-
content = verifyContent(this.subsectionExtractor.extractSubsection(content, contentType));
154+
content = verifyContent(
155+
this.subsectionExtractor.extractSubsection(content, contentType, this.fieldDescriptors));
155156
}
156157
ContentHandler contentHandler = ContentHandler.forContentWithDescriptors(content, contentType,
157158
this.fieldDescriptors);

spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java

+29-8
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
package org.springframework.restdocs.payload;
1818

1919
import java.io.IOException;
20-
import java.util.ArrayList;
20+
import java.util.Collections;
2121
import java.util.List;
22+
import java.util.Map;
2223
import java.util.Set;
24+
import java.util.stream.Collectors;
2325

2426
import com.fasterxml.jackson.databind.ObjectMapper;
2527
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -71,28 +73,39 @@ protected FieldPathPayloadSubsectionExtractor(String fieldPath, String subsectio
7173

7274
@Override
7375
public byte[] extractSubsection(byte[] payload, MediaType contentType) {
76+
return extractSubsection(payload, contentType, Collections.emptyList());
77+
}
78+
79+
@Override
80+
public byte[] extractSubsection(byte[] payload, MediaType contentType, List<FieldDescriptor> descriptors) {
7481
try {
7582
ExtractedField extractedField = new JsonFieldProcessor().extract(this.fieldPath,
7683
objectMapper.readValue(payload, Object.class));
7784
Object value = extractedField.getValue();
7885
if (value == ExtractedField.ABSENT) {
7986
throw new PayloadHandlingException(this.fieldPath + " does not identify a section of the payload");
8087
}
88+
Map<JsonFieldPath, FieldDescriptor> descriptorsByPath = descriptors.stream()
89+
.collect(Collectors.toMap(
90+
(descriptor) -> JsonFieldPath.compile(this.fieldPath + "." + descriptor.getPath()),
91+
this::prependFieldPath));
8192
if (value instanceof List) {
8293
List<?> extractedList = (List<?>) value;
83-
Set<String> uncommonPaths = JsonFieldPaths.from(extractedList).getUncommon();
94+
JsonContentHandler contentHandler = new JsonContentHandler(payload, descriptorsByPath.values());
95+
Set<JsonFieldPath> uncommonPaths = JsonFieldPaths.from(extractedList).getUncommon().stream()
96+
.map((path) -> JsonFieldPath.compile(this.fieldPath + "." + path)).filter((path) -> {
97+
FieldDescriptor descriptorForPath = descriptorsByPath.getOrDefault(path,
98+
new FieldDescriptor(path.toString()));
99+
return contentHandler.isMissing(descriptorForPath);
100+
}).collect(Collectors.toSet());
84101
if (uncommonPaths.isEmpty()) {
85102
value = extractedList.get(0);
86103
}
87104
else {
88105
String message = this.fieldPath + " identifies multiple sections of "
89106
+ "the payload and they do not have a common structure. The "
90-
+ "following uncommon paths were found: ";
91-
List<String> prefixedPaths = new ArrayList<>();
92-
for (String uncommonPath : uncommonPaths) {
93-
prefixedPaths.add(this.fieldPath + "." + uncommonPath);
94-
}
95-
message += prefixedPaths;
107+
+ "following non-optional uncommon paths were found: ";
108+
message += uncommonPaths;
96109
throw new PayloadHandlingException(message);
97110
}
98111
}
@@ -103,6 +116,14 @@ public byte[] extractSubsection(byte[] payload, MediaType contentType) {
103116
}
104117
}
105118

119+
private FieldDescriptor prependFieldPath(FieldDescriptor original) {
120+
FieldDescriptor prefixed = new FieldDescriptor(this.fieldPath + "." + original.getPath());
121+
if (original.isOptional()) {
122+
prefixed.optional();
123+
}
124+
return prefixed;
125+
}
126+
106127
@Override
107128
public String getSubsectionId() {
108129
return this.subsectionId;

spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java

+12-8
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ class JsonContentHandler implements ContentHandler {
4444

4545
private final byte[] rawContent;
4646

47-
private final List<FieldDescriptor> fieldDescriptors;
47+
private final Collection<FieldDescriptor> fieldDescriptors;
4848

49-
JsonContentHandler(byte[] content, List<FieldDescriptor> fieldDescriptors) {
49+
JsonContentHandler(byte[] content, Collection<FieldDescriptor> fieldDescriptors) {
5050
this.rawContent = content;
5151
this.fieldDescriptors = fieldDescriptors;
5252
readContent();
@@ -55,22 +55,26 @@ class JsonContentHandler implements ContentHandler {
5555
@Override
5656
public List<FieldDescriptor> findMissingFields() {
5757
List<FieldDescriptor> missingFields = new ArrayList<>();
58-
Object payload = readContent();
5958
for (FieldDescriptor fieldDescriptor : this.fieldDescriptors) {
60-
if (!fieldDescriptor.isOptional() && !this.fieldProcessor.hasField(fieldDescriptor.getPath(), payload)
61-
&& !isNestedBeneathMissingOptionalField(fieldDescriptor, payload)) {
59+
if (isMissing(fieldDescriptor)) {
6260
missingFields.add(fieldDescriptor);
6361
}
6462
}
6563

6664
return missingFields;
6765
}
6866

69-
private boolean isNestedBeneathMissingOptionalField(FieldDescriptor missing, Object payload) {
67+
boolean isMissing(FieldDescriptor descriptor) {
68+
Object payload = readContent();
69+
return !descriptor.isOptional() && !this.fieldProcessor.hasField(descriptor.getPath(), payload)
70+
&& !isNestedBeneathMissingOptionalField(descriptor, payload);
71+
}
72+
73+
private boolean isNestedBeneathMissingOptionalField(FieldDescriptor descriptor, Object payload) {
7074
List<FieldDescriptor> candidates = new ArrayList<>(this.fieldDescriptors);
71-
candidates.remove(missing);
75+
candidates.remove(descriptor);
7276
for (FieldDescriptor candidate : candidates) {
73-
if (candidate.isOptional() && missing.getPath().startsWith(candidate.getPath())
77+
if (candidate.isOptional() && descriptor.getPath().startsWith(candidate.getPath())
7478
&& isMissing(candidate, payload)) {
7579
return true;
7680
}

spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java

+20
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,26 @@ List<String> getSegments() {
5555
return this.segments;
5656
}
5757

58+
@Override
59+
public boolean equals(Object obj) {
60+
if (this == obj) {
61+
return true;
62+
}
63+
if (obj == null) {
64+
return false;
65+
}
66+
if (getClass() != obj.getClass()) {
67+
return false;
68+
}
69+
JsonFieldPath other = (JsonFieldPath) obj;
70+
return this.segments.equals(other.segments);
71+
}
72+
73+
@Override
74+
public int hashCode() {
75+
return this.segments.hashCode();
76+
}
77+
5878
@Override
5979
public String toString() {
6080
return this.rawPath;

spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java

+15
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.restdocs.payload;
1818

19+
import java.util.List;
20+
1921
import org.springframework.http.MediaType;
2022

2123
/**
@@ -36,6 +38,19 @@ public interface PayloadSubsectionExtractor<T extends PayloadSubsectionExtractor
3638
*/
3739
byte[] extractSubsection(byte[] payload, MediaType contentType);
3840

41+
/**
42+
* Extracts a subsection of the given {@code payload} that has the given
43+
* {@code contentType} and that is described by the given {@code descriptors}.
44+
* @param payload the payload
45+
* @param contentType the content type of the payload
46+
* @param descriptors descriptors that describe the payload
47+
* @return the subsection of the payload
48+
* @since 2.0.4
49+
*/
50+
default byte[] extractSubsection(byte[] payload, MediaType contentType, List<FieldDescriptor> descriptors) {
51+
return extractSubsection(payload, contentType);
52+
}
53+
3954
/**
4055
* Returns an identifier for the subsection that this extractor will extract.
4156
* @return the identifier

spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java

+26-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.restdocs.payload;
1818

1919
import java.io.IOException;
20+
import java.util.Arrays;
2021
import java.util.Map;
2122

2223
import com.fasterxml.jackson.core.JsonParseException;
@@ -101,11 +102,35 @@ public void extractMapSubsectionWithCommonStructureFromMultiElementArrayInAJsonM
101102
public void extractMapSubsectionWithVaryingStructureFromMultiElementArrayInAJsonMap()
102103
throws JsonParseException, JsonMappingException, IOException {
103104
this.thrown.expect(PayloadHandlingException.class);
104-
this.thrown.expectMessage("The following uncommon paths were found: [a.[].b.d]");
105+
this.thrown.expectMessage("The following non-optional uncommon paths were found: [a.[].b.d]");
105106
new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection(
106107
"{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6, \"d\": 7}}]}".getBytes(), MediaType.APPLICATION_JSON);
107108
}
108109

110+
@Test
111+
@SuppressWarnings("unchecked")
112+
public void extractMapSubsectionWithVaryingStructureDueToOptionalFieldsFromMultiElementArrayInAJsonMap()
113+
throws JsonParseException, JsonMappingException, IOException {
114+
byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection(
115+
"{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6, \"d\": 7}}]}".getBytes(), MediaType.APPLICATION_JSON,
116+
Arrays.asList(new FieldDescriptor("d").optional()));
117+
Map<String, Object> extracted = new ObjectMapper().readValue(extractedPayload, Map.class);
118+
assertThat(extracted.size()).isEqualTo(1);
119+
assertThat(extracted).containsOnlyKeys("c");
120+
}
121+
122+
@Test
123+
@SuppressWarnings("unchecked")
124+
public void extractMapSubsectionWithVaryingStructureDueToOptionalParentFieldsFromMultiElementArrayInAJsonMap()
125+
throws JsonParseException, JsonMappingException, IOException {
126+
byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection(
127+
"{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6, \"d\": { \"e\": 7}}}]}".getBytes(),
128+
MediaType.APPLICATION_JSON, Arrays.asList(new FieldDescriptor("d").optional()));
129+
Map<String, Object> extracted = new ObjectMapper().readValue(extractedPayload, Map.class);
130+
assertThat(extracted.size()).isEqualTo(1);
131+
assertThat(extracted).containsOnlyKeys("c");
132+
}
133+
109134
@Test
110135
public void extractedSubsectionIsPrettyPrintedWhenInputIsPrettyPrinted()
111136
throws JsonParseException, JsonMappingException, JsonProcessingException, IOException {

0 commit comments

Comments
 (0)