Skip to content

Commit 3f66b06

Browse files
committed
Improve field type resolution for fields with optional ancestors
Fixes gh-567
1 parent 3a5cc58 commit 3f66b06

File tree

8 files changed

+176
-97
lines changed

8 files changed

+176
-97
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ protected Map<String, Object> createModel(Operation operation) {
153153
if (this.subsectionExtractor != null) {
154154
content = verifyContent(this.subsectionExtractor.extractSubsection(content, contentType));
155155
}
156-
ContentHandler contentHandler = ContentHandler.forContent(content, contentType);
156+
ContentHandler contentHandler = ContentHandler.forContentWithDescriptors(content, contentType,
157+
this.fieldDescriptors);
157158

158159
validateFieldDocumentation(contentHandler);
159160

@@ -193,10 +194,9 @@ private byte[] verifyContent(byte[] content) {
193194
}
194195

195196
private void validateFieldDocumentation(ContentHandler payloadHandler) {
196-
List<FieldDescriptor> missingFields = payloadHandler.findMissingFields(this.fieldDescriptors);
197+
List<FieldDescriptor> missingFields = payloadHandler.findMissingFields();
197198

198-
String undocumentedPayload = this.ignoreUndocumentedFields ? null
199-
: payloadHandler.getUndocumentedContent(this.fieldDescriptors);
199+
String undocumentedPayload = this.ignoreUndocumentedFields ? null : payloadHandler.getUndocumentedContent();
200200

201201
if (!missingFields.isEmpty() || StringUtils.hasText(undocumentedPayload)) {
202202
String message = "";

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,41 +30,40 @@ interface ContentHandler extends FieldTypeResolver {
3030

3131
/**
3232
* Finds the fields that are missing from the handler's payload. A field is missing if
33-
* it is described by one of the {@code fieldDescriptors} but is not present in the
34-
* payload.
35-
* @param fieldDescriptors the descriptors
33+
* it is described but is not present in the payload.
3634
* @return descriptors for the fields that are missing from the payload
3735
* @throws PayloadHandlingException if a failure occurs
3836
*/
39-
List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors);
37+
List<FieldDescriptor> findMissingFields();
4038

4139
/**
4240
* Returns modified content, formatted as a String, that only contains the fields that
4341
* are undocumented. A field is undocumented if it is present in the handler's content
44-
* but is not described by the given {@code fieldDescriptors}. If the content is
45-
* completely documented, {@code null} is returned
46-
* @param fieldDescriptors the descriptors
42+
* but is not described. If the content is completely documented, {@code null} is
43+
* returned
4744
* @return the undocumented content, or {@code null} if all of the content is
4845
* documented
4946
* @throws PayloadHandlingException if a failure occurs
5047
*/
51-
String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors);
48+
String getUndocumentedContent();
5249

5350
/**
54-
* Create a {@link ContentHandler} for the given content type and payload.
51+
* Create a {@link ContentHandler} for the given content type and payload, described
52+
* by the given descriptors.
5553
* @param content the payload
5654
* @param contentType the content type
55+
* @param descriptors descriptors of the content
5756
* @return the ContentHandler
5857
* @throws PayloadHandlingException if no known ContentHandler can handle the content
5958
*/
60-
static ContentHandler forContent(byte[] content, MediaType contentType) {
61-
59+
static ContentHandler forContentWithDescriptors(byte[] content, MediaType contentType,
60+
List<FieldDescriptor> descriptors) {
6261
try {
63-
return new JsonContentHandler(content);
62+
return new JsonContentHandler(content, descriptors);
6463
}
6564
catch (Exception je) {
6665
try {
67-
return new XmlContentHandler(content);
66+
return new XmlContentHandler(content, descriptors);
6867
}
6968
catch (Exception xe) {
7069
throw new PayloadHandlingException(

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2018 the original author or authors.
2+
* Copyright 2014-2019 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.
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.restdocs.payload;
1818

19+
import java.util.Collections;
20+
import java.util.List;
21+
1922
import org.springframework.http.MediaType;
2023

2124
/**
@@ -33,9 +36,25 @@ public interface FieldTypeResolver {
3336
* @param content the payload that the {@code FieldTypeResolver} should handle
3437
* @param contentType the content type of the payload
3538
* @return the {@code FieldTypeResolver}
39+
* @deprecated since 2.0.4 in favor of
40+
* {@link #forContentWithDescriptors(byte[], MediaType, List)}
3641
*/
42+
@Deprecated
3743
static FieldTypeResolver forContent(byte[] content, MediaType contentType) {
38-
return ContentHandler.forContent(content, contentType);
44+
return forContentWithDescriptors(content, contentType, Collections.emptyList());
45+
}
46+
47+
/**
48+
* Create a {@code FieldTypeResolver} for the given {@code content} and
49+
* {@code contentType}, described by the given {@code descriptors}.
50+
* @param content the payload that the {@code FieldTypeResolver} should handle
51+
* @param contentType the content type of the payload
52+
* @param descriptors the descriptors of the content
53+
* @return the {@code FieldTypeResolver}
54+
*/
55+
static FieldTypeResolver forContentWithDescriptors(byte[] content, MediaType contentType,
56+
List<FieldDescriptor> descriptors) {
57+
return ContentHandler.forContentWithDescriptors(content, contentType, descriptors);
3958
}
4059

4160
/**

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,28 +44,30 @@ class JsonContentHandler implements ContentHandler {
4444

4545
private final byte[] rawContent;
4646

47-
JsonContentHandler(byte[] content) {
47+
private final List<FieldDescriptor> fieldDescriptors;
48+
49+
JsonContentHandler(byte[] content, List<FieldDescriptor> fieldDescriptors) {
4850
this.rawContent = content;
51+
this.fieldDescriptors = fieldDescriptors;
4952
readContent();
5053
}
5154

5255
@Override
53-
public List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors) {
56+
public List<FieldDescriptor> findMissingFields() {
5457
List<FieldDescriptor> missingFields = new ArrayList<>();
5558
Object payload = readContent();
56-
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
59+
for (FieldDescriptor fieldDescriptor : this.fieldDescriptors) {
5760
if (!fieldDescriptor.isOptional() && !this.fieldProcessor.hasField(fieldDescriptor.getPath(), payload)
58-
&& !isNestedBeneathMissingOptionalField(fieldDescriptor, fieldDescriptors, payload)) {
61+
&& !isNestedBeneathMissingOptionalField(fieldDescriptor, payload)) {
5962
missingFields.add(fieldDescriptor);
6063
}
6164
}
6265

6366
return missingFields;
6467
}
6568

66-
private boolean isNestedBeneathMissingOptionalField(FieldDescriptor missing, List<FieldDescriptor> fieldDescriptors,
67-
Object payload) {
68-
List<FieldDescriptor> candidates = new ArrayList<>(fieldDescriptors);
69+
private boolean isNestedBeneathMissingOptionalField(FieldDescriptor missing, Object payload) {
70+
List<FieldDescriptor> candidates = new ArrayList<>(this.fieldDescriptors);
6971
candidates.remove(missing);
7072
for (FieldDescriptor candidate : candidates) {
7173
if (candidate.isOptional() && missing.getPath().startsWith(candidate.getPath())
@@ -98,9 +100,9 @@ private boolean isEmptyCollection(Object value) {
98100
}
99101

100102
@Override
101-
public String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors) {
103+
public String getUndocumentedContent() {
102104
Object content = readContent();
103-
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
105+
for (FieldDescriptor fieldDescriptor : this.fieldDescriptors) {
104106
if (describesSubsection(fieldDescriptor)) {
105107
this.fieldProcessor.removeSubsection(fieldDescriptor.getPath(), content);
106108
}
@@ -154,7 +156,9 @@ public Object resolveFieldType(FieldDescriptor fieldDescriptor) {
154156
.discoverFieldTypes(fieldDescriptor.getPath(), readContent())
155157
.coalesce(fieldDescriptor.isOptional());
156158
if (descriptorFieldType == JsonFieldType.VARIES || descriptorFieldType == actualFieldType
157-
|| (fieldDescriptor.isOptional() && actualFieldType == JsonFieldType.NULL)) {
159+
|| (fieldDescriptor.isOptional() && actualFieldType == JsonFieldType.NULL)
160+
|| (isNestedBeneathMissingOptionalField(fieldDescriptor, readContent())
161+
&& actualFieldType == JsonFieldType.VARIES)) {
158162
return descriptorFieldType;
159163
}
160164
throw new FieldTypesDoNotMatchException(fieldDescriptor, actualFieldType);

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,25 @@ class XmlContentHandler implements ContentHandler {
5353

5454
private final byte[] rawContent;
5555

56-
XmlContentHandler(byte[] rawContent) {
56+
private final List<FieldDescriptor> fieldDescriptors;
57+
58+
XmlContentHandler(byte[] rawContent, List<FieldDescriptor> fieldDescriptors) {
5759
try {
5860
this.documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
5961
}
6062
catch (ParserConfigurationException ex) {
6163
throw new IllegalStateException("Failed to create document builder", ex);
6264
}
6365
this.rawContent = rawContent;
66+
this.fieldDescriptors = fieldDescriptors;
6467
readPayload();
6568
}
6669

6770
@Override
68-
public List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors) {
71+
public List<FieldDescriptor> findMissingFields() {
6972
List<FieldDescriptor> missingFields = new ArrayList<>();
7073
Document payload = readPayload();
71-
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
74+
for (FieldDescriptor fieldDescriptor : this.fieldDescriptors) {
7275
if (!fieldDescriptor.isOptional()) {
7376
NodeList matchingNodes = findMatchingNodes(fieldDescriptor, payload);
7477
if (matchingNodes.getLength() == 0) {
@@ -103,10 +106,10 @@ private XPathExpression createXPath(String fieldPath) throws XPathExpressionExce
103106
}
104107

105108
@Override
106-
public String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors) {
109+
public String getUndocumentedContent() {
107110
Document payload = readPayload();
108111
List<Node> matchedButNotRemoved = new ArrayList<>();
109-
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
112+
for (FieldDescriptor fieldDescriptor : this.fieldDescriptors) {
110113
NodeList matchingNodes;
111114
try {
112115
matchingNodes = (NodeList) createXPath(fieldDescriptor.getPath()).evaluate(payload,

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

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.restdocs.payload;
1818

19+
import java.util.Collections;
20+
1921
import org.junit.Rule;
2022
import org.junit.Test;
2123
import org.junit.rules.ExpectedException;
@@ -35,21 +37,43 @@ public class FieldTypeResolverTests {
3537
public ExpectedException thrownException = ExpectedException.none();
3638

3739
@Test
38-
public void returnJsonFieldTypeResolver() {
40+
@Deprecated
41+
public void whenForContentCalledWithJsonContentThenReturnsJsonFieldTypeResolver() {
3942
assertThat(FieldTypeResolver.forContent("{\"field\": \"value\"}".getBytes(), MediaType.APPLICATION_JSON))
4043
.isInstanceOf(JsonContentHandler.class);
4144
}
4245

4346
@Test
44-
public void returnXmlContentHandler() {
47+
@Deprecated
48+
public void whenForContentCalledWithXmlContentThenReturnsXmlContentHandler() {
4549
assertThat(FieldTypeResolver.forContent("<a><b>5</b></a>".getBytes(), MediaType.APPLICATION_XML))
4650
.isInstanceOf(XmlContentHandler.class);
4751
}
4852

4953
@Test
50-
public void throwOnInvalidContent() {
54+
@Deprecated
55+
public void whenForContentIsCalledWithInvalidContentThenExceptionIsThrown() {
5156
this.thrownException.expect(PayloadHandlingException.class);
5257
FieldTypeResolver.forContent("some".getBytes(), MediaType.APPLICATION_XML);
5358
}
5459

60+
@Test
61+
public void whenForContentWithDescriptorsCalledWithJsonContentThenReturnsJsonFieldTypeResolver() {
62+
assertThat(FieldTypeResolver.forContentWithDescriptors("{\"field\": \"value\"}".getBytes(),
63+
MediaType.APPLICATION_JSON, Collections.emptyList())).isInstanceOf(JsonContentHandler.class);
64+
}
65+
66+
@Test
67+
public void whenForContentWithDescriptorsCalledWithXmlContentThenReturnsXmlContentHandler() {
68+
assertThat(FieldTypeResolver.forContentWithDescriptors("<a><b>5</b></a>".getBytes(), MediaType.APPLICATION_XML,
69+
Collections.emptyList())).isInstanceOf(XmlContentHandler.class);
70+
}
71+
72+
@Test
73+
public void whenForContentWithDescriptorsIsCalledWithInvalidContentThenExceptionIsThrown() {
74+
this.thrownException.expect(PayloadHandlingException.class);
75+
FieldTypeResolver.forContentWithDescriptors("some".getBytes(), MediaType.APPLICATION_XML,
76+
Collections.emptyList());
77+
}
78+
5579
}

0 commit comments

Comments
 (0)