Skip to content

Commit ada273b

Browse files
committed
Rework JsonFieldTypeResolver to only discover the field types
See spring-projectsgh-549
1 parent 9deae49 commit ada273b

File tree

8 files changed

+402
-314
lines changed

8 files changed

+402
-314
lines changed

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class JsonContentHandler implements ContentHandler {
3737

3838
private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
3939

40-
private final JsonFieldTypeResolver fieldTypeResolver = new JsonFieldTypeResolver();
40+
private final JsonFieldTypesDiscoverer fieldTypesDiscoverer = new JsonFieldTypesDiscoverer();
4141

4242
private final ObjectMapper objectMapper = new ObjectMapper()
4343
.enable(SerializationFeature.INDENT_OUTPUT);
@@ -147,16 +147,18 @@ private boolean isEmpty(Object object) {
147147
@Override
148148
public Object determineFieldType(FieldDescriptor fieldDescriptor) {
149149
if (fieldDescriptor.getType() == null) {
150-
return this.fieldTypeResolver.resolveFieldType(fieldDescriptor,
151-
readContent());
150+
return this.fieldTypesDiscoverer
151+
.discoverFieldTypes(fieldDescriptor.getPath(), readContent())
152+
.coalesce(fieldDescriptor.isOptional());
152153
}
153154
if (!(fieldDescriptor.getType() instanceof JsonFieldType)) {
154155
return fieldDescriptor.getType();
155156
}
156157
JsonFieldType descriptorFieldType = (JsonFieldType) fieldDescriptor.getType();
157158
try {
158-
JsonFieldType actualFieldType = this.fieldTypeResolver
159-
.resolveFieldType(fieldDescriptor, readContent());
159+
JsonFieldType actualFieldType = this.fieldTypesDiscoverer
160+
.discoverFieldTypes(fieldDescriptor.getPath(), readContent())
161+
.coalesce(fieldDescriptor.isOptional());
160162
if (descriptorFieldType == JsonFieldType.VARIES
161163
|| descriptorFieldType == actualFieldType
162164
|| (fieldDescriptor.isOptional()

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,25 @@ boolean hasField(String path, Object payload) {
4141

4242
ExtractedField extract(String path, Object payload) {
4343
JsonFieldPath compiledPath = JsonFieldPath.compile(path);
44-
final List<Object> matches = new ArrayList<>();
44+
final List<Object> values = new ArrayList<>();
4545
traverse(new ProcessingContext(payload, compiledPath), new MatchCallback() {
4646

4747
@Override
4848
public void foundMatch(Match match) {
49-
matches.add(match.getValue());
49+
values.add(match.getValue());
50+
}
51+
52+
@Override
53+
public void absent() {
54+
values.add(ExtractedField.ABSENT);
5055
}
5156

5257
});
53-
if (matches.isEmpty()) {
54-
throw new FieldDoesNotExistException(path);
58+
if (values.isEmpty()) {
59+
values.add(ExtractedField.ABSENT);
5560
}
5661
return new ExtractedField(
57-
(compiledPath.getType() != PathType.SINGLE) ? matches : matches.get(0),
62+
(compiledPath.getType() != PathType.SINGLE) ? values : values.get(0),
5863
compiledPath.getType());
5964
}
6065

@@ -427,6 +432,8 @@ private ProcessingContext descend(Object payload, Match match) {
427432
*/
428433
static class ExtractedField {
429434

435+
static final Object ABSENT = new Object();
436+
430437
private final Object value;
431438

432439
private final PathType type;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2014-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.restdocs.payload;
18+
19+
import java.util.Collections;
20+
import java.util.HashSet;
21+
import java.util.Iterator;
22+
import java.util.Set;
23+
24+
/**
25+
* {@link JsonFieldType Types} for a field discovered in a JSON payload.
26+
*
27+
* @author Andy Wilkinson
28+
*/
29+
class JsonFieldTypes implements Iterable<JsonFieldType> {
30+
31+
private final Set<JsonFieldType> fieldTypes;
32+
33+
JsonFieldTypes(JsonFieldType fieldType) {
34+
this(Collections.singleton(fieldType));
35+
}
36+
37+
JsonFieldTypes(Set<JsonFieldType> fieldTypes) {
38+
this.fieldTypes = fieldTypes;
39+
}
40+
41+
JsonFieldType coalesce(boolean optional) {
42+
Set<JsonFieldType> types = new HashSet<>(this.fieldTypes);
43+
if (optional && types.size() > 1) {
44+
types.remove(JsonFieldType.NULL);
45+
}
46+
if (types.size() == 1) {
47+
return types.iterator().next();
48+
}
49+
return JsonFieldType.VARIES;
50+
}
51+
52+
@Override
53+
public Iterator<JsonFieldType> iterator() {
54+
return this.fieldTypes.iterator();
55+
}
56+
57+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2017 the original author or authors.
2+
* Copyright 2014-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,50 +17,45 @@
1717
package org.springframework.restdocs.payload;
1818

1919
import java.util.Collection;
20+
import java.util.HashSet;
2021
import java.util.Map;
22+
import java.util.Set;
2123

2224
import org.springframework.restdocs.payload.JsonFieldPath.PathType;
2325
import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField;
2426

2527
/**
26-
* Resolves the type of a field in a JSON request or response payload.
28+
* Discovers the types of the fields found at a path in a JSON request or response
29+
* payload.
2730
*
2831
* @author Andy Wilkinson
2932
*/
30-
class JsonFieldTypeResolver {
33+
class JsonFieldTypesDiscoverer {
3134

3235
private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
3336

34-
JsonFieldType resolveFieldType(FieldDescriptor fieldDescriptor, Object payload) {
35-
ExtractedField extractedField = this.fieldProcessor
36-
.extract(fieldDescriptor.getPath(), payload);
37+
JsonFieldTypes discoverFieldTypes(String path, Object payload) {
38+
ExtractedField extractedField = this.fieldProcessor.extract(path, payload);
3739
Object value = extractedField.getValue();
3840
if (value instanceof Collection && extractedField.getType() == PathType.MULTI) {
39-
JsonFieldType commonType = null;
40-
for (Object item : (Collection<?>) value) {
41-
JsonFieldType fieldType = determineFieldType(item);
42-
if (commonType == null) {
43-
commonType = fieldType;
44-
}
45-
else if (fieldType != commonType) {
46-
if (!fieldDescriptor.isOptional()) {
47-
return JsonFieldType.VARIES;
48-
}
49-
if (commonType == JsonFieldType.NULL) {
50-
commonType = fieldType;
51-
}
52-
else if (fieldType != JsonFieldType.NULL) {
53-
return JsonFieldType.VARIES;
54-
}
55-
}
41+
Collection<?> values = (Collection<?>) value;
42+
if (allAbsent(values)) {
43+
throw new FieldDoesNotExistException(path);
5644
}
57-
return commonType;
45+
Set<JsonFieldType> fieldTypes = new HashSet<>();
46+
for (Object item : values) {
47+
fieldTypes.add(determineFieldType(item));
48+
}
49+
return new JsonFieldTypes(fieldTypes);
50+
}
51+
if (value == ExtractedField.ABSENT) {
52+
throw new FieldDoesNotExistException(path);
5853
}
59-
return determineFieldType(value);
54+
return new JsonFieldTypes(determineFieldType(value));
6055
}
6156

6257
private JsonFieldType determineFieldType(Object fieldValue) {
63-
if (fieldValue == null) {
58+
if (fieldValue == null || fieldValue == ExtractedField.ABSENT) {
6459
return JsonFieldType.NULL;
6560
}
6661
if (fieldValue instanceof String) {
@@ -78,4 +73,13 @@ private JsonFieldType determineFieldType(Object fieldValue) {
7873
return JsonFieldType.NUMBER;
7974
}
8075

76+
private boolean allAbsent(Collection<?> values) {
77+
for (Object value : values) {
78+
if (value != ExtractedField.ABSENT) {
79+
return false;
80+
}
81+
}
82+
return true;
83+
}
84+
8185
}

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

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import com.fasterxml.jackson.databind.ObjectMapper;
2929
import org.junit.Test;
3030

31+
import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField;
32+
3133
import static org.assertj.core.api.Assertions.assertThat;
3234

3335
/**
@@ -109,7 +111,7 @@ public void extractOccasionallyAbsentFieldFromItemsInArray() {
109111
new HashMap<String, Object>());
110112
payload.put("a", alpha);
111113
assertThat(this.fieldProcessor.extract("a[].b", payload).getValue())
112-
.isEqualTo(Arrays.asList("bravo"));
114+
.isEqualTo(Arrays.asList("bravo", ExtractedField.ABSENT));
113115
}
114116

115117
@Test
@@ -165,54 +167,61 @@ public void extractArraysFromItemsInNestedArray() {
165167
Arrays.asList(4)));
166168
}
167169

168-
@Test(expected = FieldDoesNotExistException.class)
170+
@Test
169171
public void nonExistentTopLevelField() {
170-
this.fieldProcessor.extract("a", Collections.emptyMap());
172+
assertThat(this.fieldProcessor.extract("a", Collections.emptyMap()).getValue())
173+
.isEqualTo(ExtractedField.ABSENT);
171174
}
172175

173-
@Test(expected = FieldDoesNotExistException.class)
176+
@Test
174177
public void nonExistentNestedField() {
175178
HashMap<String, Object> payload = new HashMap<>();
176179
payload.put("a", new HashMap<String, Object>());
177-
this.fieldProcessor.extract("a.b", payload);
180+
assertThat(this.fieldProcessor.extract("a.b", payload).getValue())
181+
.isEqualTo(ExtractedField.ABSENT);
178182
}
179183

180-
@Test(expected = FieldDoesNotExistException.class)
184+
@Test
181185
public void nonExistentNestedFieldWhenParentIsNotAMap() {
182186
HashMap<String, Object> payload = new HashMap<>();
183187
payload.put("a", 5);
184-
this.fieldProcessor.extract("a.b", payload);
188+
assertThat(this.fieldProcessor.extract("a.b", payload).getValue())
189+
.isEqualTo(ExtractedField.ABSENT);
185190
}
186191

187-
@Test(expected = FieldDoesNotExistException.class)
192+
@Test
188193
public void nonExistentFieldWhenParentIsAnArray() {
189194
HashMap<String, Object> payload = new HashMap<>();
190195
HashMap<String, Object> alpha = new HashMap<>();
191196
alpha.put("b", Arrays.asList(new HashMap<String, Object>()));
192197
payload.put("a", alpha);
193-
this.fieldProcessor.extract("a.b.c", payload);
198+
assertThat(this.fieldProcessor.extract("a.b.c", payload).getValue())
199+
.isEqualTo(ExtractedField.ABSENT);
194200
}
195201

196-
@Test(expected = FieldDoesNotExistException.class)
202+
@Test
197203
public void nonExistentArrayField() {
198204
HashMap<String, Object> payload = new HashMap<>();
199-
this.fieldProcessor.extract("a[]", payload);
205+
assertThat(this.fieldProcessor.extract("a[]", payload).getValue())
206+
.isEqualTo(ExtractedField.ABSENT);
200207
}
201208

202-
@Test(expected = FieldDoesNotExistException.class)
209+
@Test
203210
public void nonExistentArrayFieldAsTypeDoesNotMatch() {
204211
HashMap<String, Object> payload = new HashMap<>();
205212
payload.put("a", 5);
206-
this.fieldProcessor.extract("a[]", payload);
213+
assertThat(this.fieldProcessor.extract("a[]", payload).getValue())
214+
.isEqualTo(ExtractedField.ABSENT);
207215
}
208216

209-
@Test(expected = FieldDoesNotExistException.class)
217+
@Test
210218
public void nonExistentFieldBeneathAnArray() {
211219
HashMap<String, Object> payload = new HashMap<>();
212220
HashMap<String, Object> alpha = new HashMap<>();
213221
alpha.put("b", Arrays.asList(new HashMap<String, Object>()));
214222
payload.put("a", alpha);
215-
this.fieldProcessor.extract("a.b[].id", payload);
223+
assertThat(this.fieldProcessor.extract("a.b[].id", payload).getValue())
224+
.isEqualTo(Arrays.asList(ExtractedField.ABSENT));
216225
}
217226

218227
@Test

0 commit comments

Comments
 (0)