Skip to content

Commit 043e796

Browse files
committed
Introduce relaxed snippets that don’t fail when item is not documented
Previously, many of the snippets would fail when something wasn’t documented. This worked well when writing exhaustive API documentation, but was cumbersome when trying to document a scenario that might only being interested in a subset of the links, response fields, etc. It was necessary to mark things that were not of interest as being ignored. This commit introduces a relaxed variant of several snippets. A relaxed snippet will not fail if something has not been documented. Instead, the undocumented thing will be ignored. If something has been documented but it does not exist a failure will still occur. Closes gh-175
1 parent 0e52ef0 commit 043e796

15 files changed

+559
-28
lines changed

spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,26 @@ public static LinksSnippet links(LinkDescriptor... descriptors) {
6666
Arrays.asList(descriptors));
6767
}
6868

69+
/**
70+
* Returns a new {@code Snippet} that will document the links in the API operation's
71+
* response. Links will be extracted from the response automatically based on its
72+
* content type and will be documented using the given {@code descriptors}.
73+
* <p>
74+
* If a link is documented, is not marked as optional, and is not present in the
75+
* response, a failure will occur. Any undocumented links will be ignored.
76+
* <p>
77+
* If a descriptor does not have a {@link LinkDescriptor#description(Object)
78+
* description}, the {@link Link#getTitle() title} of the link will be used. If the
79+
* link does not have a title a failure will occur.
80+
*
81+
* @param descriptors the descriptions of the response's links
82+
* @return the snippet that will document the links
83+
*/
84+
public static LinksSnippet relaxedLinks(LinkDescriptor... descriptors) {
85+
return new LinksSnippet(new ContentTypeLinkExtractor(),
86+
Arrays.asList(descriptors), true);
87+
}
88+
6989
/**
7090
* Returns a new {@code Snippet} that will document the links in the API call's
7191
* response. The given {@code attributes} will be available during snippet generation.
@@ -80,6 +100,10 @@ public static LinksSnippet links(LinkDescriptor... descriptors) {
80100
* If you do not want to document a link, a link descriptor can be marked as
81101
* {@link LinkDescriptor#ignored}. This will prevent it from appearing in the
82102
* generated snippet while avoiding the failure described above.
103+
* <p>
104+
* If a descriptor does not have a {@link LinkDescriptor#description(Object)
105+
* description}, the {@link Link#getTitle() title} of the link will be used. If the
106+
* link does not have a title a failure will occur.
83107
*
84108
* @param attributes the attributes
85109
* @param descriptors the descriptions of the response's links
@@ -91,6 +115,29 @@ public static LinksSnippet links(Map<String, Object> attributes,
91115
Arrays.asList(descriptors), attributes);
92116
}
93117

118+
/**
119+
* Returns a new {@code Snippet} that will document the links in the API call's
120+
* response. The given {@code attributes} will be available during snippet generation.
121+
* Links will be extracted from the response automatically based on its content type
122+
* and will be documented using the given {@code descriptors}.
123+
* <p>
124+
* If a link is documented, is not marked as optional, and is not present in the
125+
* response, a failure will occur. Any undocumented links will be ignored.
126+
* <p>
127+
* If a descriptor does not have a {@link LinkDescriptor#description(Object)
128+
* description}, the {@link Link#getTitle() title} of the link will be used. If the
129+
* link does not have a title a failure will occur.
130+
*
131+
* @param attributes the attributes
132+
* @param descriptors the descriptions of the response's links
133+
* @return the snippet that will document the links
134+
*/
135+
public static LinksSnippet relaxedLinks(Map<String, Object> attributes,
136+
LinkDescriptor... descriptors) {
137+
return new LinksSnippet(new ContentTypeLinkExtractor(),
138+
Arrays.asList(descriptors), attributes, true);
139+
}
140+
94141
/**
95142
* Returns a new {@code Snippet} that will document the links in the API operation's
96143
* response. Links will be extracted from the response using the given
@@ -104,6 +151,10 @@ public static LinksSnippet links(Map<String, Object> attributes,
104151
* If you do not want to document a link, a link descriptor can be marked as
105152
* {@link LinkDescriptor#ignored}. This will prevent it from appearing in the
106153
* generated snippet while avoiding the failure described above.
154+
* <p>
155+
* If a descriptor does not have a {@link LinkDescriptor#description(Object)
156+
* description}, the {@link Link#getTitle() title} of the link will be used. If the
157+
* link does not have a title a failure will occur.
107158
*
108159
* @param linkExtractor used to extract the links from the response
109160
* @param descriptors the descriptions of the response's links
@@ -114,6 +165,27 @@ public static LinksSnippet links(LinkExtractor linkExtractor,
114165
return new LinksSnippet(linkExtractor, Arrays.asList(descriptors));
115166
}
116167

168+
/**
169+
* Returns a new {@code Snippet} that will document the links in the API operation's
170+
* response. Links will be extracted from the response using the given
171+
* {@code linkExtractor} and will be documented using the given {@code descriptors}.
172+
* <p>
173+
* If a link is documented, is not marked as optional, and is not present in the
174+
* response, a failure will occur. Any undocumented links will be ignored.
175+
* <p>
176+
* If a descriptor does not have a {@link LinkDescriptor#description(Object)
177+
* description}, the {@link Link#getTitle() title} of the link will be used. If the
178+
* link does not have a title a failure will occur.
179+
*
180+
* @param linkExtractor used to extract the links from the response
181+
* @param descriptors the descriptions of the response's links
182+
* @return the snippet that will document the links
183+
*/
184+
public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor,
185+
LinkDescriptor... descriptors) {
186+
return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), true);
187+
}
188+
117189
/**
118190
* Returns a new {@code Snippet} that will document the links in the API operation's
119191
* response. The given {@code attributes} will be available during snippet generation.
@@ -128,6 +200,10 @@ public static LinksSnippet links(LinkExtractor linkExtractor,
128200
* If you do not want to document a link, a link descriptor can be marked as
129201
* {@link LinkDescriptor#ignored}. This will prevent it from appearing in the
130202
* generated snippet while avoiding the failure described above.
203+
* <p>
204+
* If a descriptor does not have a {@link LinkDescriptor#description(Object)
205+
* description}, the {@link Link#getTitle() title} of the link will be used. If the
206+
* link does not have a title a failure will occur.
131207
*
132208
* @param attributes the attributes
133209
* @param linkExtractor used to extract the links from the response
@@ -139,6 +215,30 @@ public static LinksSnippet links(LinkExtractor linkExtractor,
139215
return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes);
140216
}
141217

218+
/**
219+
* Returns a new {@code Snippet} that will document the links in the API operation's
220+
* response. The given {@code attributes} will be available during snippet generation.
221+
* Links will be extracted from the response using the given {@code linkExtractor} and
222+
* will be documented using the given {@code descriptors}.
223+
* <p>
224+
* If a link is documented, is not marked as optional, and is not present in the
225+
* response, a failure will occur. Any undocumented links will be ignored.
226+
* <p>
227+
* If a descriptor does not have a {@link LinkDescriptor#description(Object)
228+
* description}, the {@link Link#getTitle() title} of the link will be used. If the
229+
* link does not have a title a failure will occur.
230+
*
231+
* @param attributes the attributes
232+
* @param linkExtractor used to extract the links from the response
233+
* @param descriptors the descriptions of the response's links
234+
* @return the snippet that will document the links
235+
*/
236+
public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor,
237+
Map<String, Object> attributes, LinkDescriptor... descriptors) {
238+
return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes,
239+
true);
240+
}
241+
142242
/**
143243
* Returns a {@code LinkExtractor} capable of extracting links in Hypermedia
144244
* Application Language (HAL) format where the links are found in a map named

spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
22+
import java.util.Collections;
2223
import java.util.HashMap;
2324
import java.util.HashSet;
2425
import java.util.LinkedHashMap;
@@ -50,35 +51,72 @@ public class LinksSnippet extends TemplatedSnippet {
5051

5152
private final LinkExtractor linkExtractor;
5253

54+
private final boolean ignoreUndocumentedLinks;
55+
5356
/**
5457
* Creates a new {@code LinksSnippet} that will extract links using the given
5558
* {@code linkExtractor} and document them using the given {@code descriptors}.
59+
* Undocumented links will trigger a failure.
5660
*
5761
* @param linkExtractor the link extractor
5862
* @param descriptors the link descriptors
5963
*/
6064
protected LinksSnippet(LinkExtractor linkExtractor,
6165
List<LinkDescriptor> descriptors) {
62-
this(linkExtractor, descriptors, null);
66+
this(linkExtractor, descriptors, null, false);
67+
}
68+
69+
/**
70+
* Creates a new {@code LinksSnippet} that will extract links using the given
71+
* {@code linkExtractor} and document them using the given {@code descriptors}. If
72+
* {@code ignoreUndocumentedLinks} is {@code true}, undocumented links will be ignored
73+
* and will not trigger a failure.
74+
*
75+
* @param linkExtractor the link extractor
76+
* @param descriptors the link descriptors
77+
* @param ignoreUndocumentedLinks whether undocumented links should be ignored
78+
*/
79+
protected LinksSnippet(LinkExtractor linkExtractor, List<LinkDescriptor> descriptors,
80+
boolean ignoreUndocumentedLinks) {
81+
this(linkExtractor, descriptors, null, ignoreUndocumentedLinks);
6382
}
6483

6584
/**
6685
* Creates a new {@code LinksSnippet} that will extract links using the given
6786
* {@code linkExtractor} and document them using the given {@code descriptors}. The
6887
* given {@code attributes} will be included in the model during template rendering.
88+
* Undocumented links will trigger a failure.
6989
*
7090
* @param linkExtractor the link extractor
7191
* @param descriptors the link descriptors
7292
* @param attributes the additional attributes
7393
*/
7494
protected LinksSnippet(LinkExtractor linkExtractor, List<LinkDescriptor> descriptors,
7595
Map<String, Object> attributes) {
96+
this(linkExtractor, descriptors, attributes, false);
97+
}
98+
99+
/**
100+
* Creates a new {@code LinksSnippet} that will extract links using the given
101+
* {@code linkExtractor} and document them using the given {@code descriptors}. The
102+
* given {@code attributes} will be included in the model during template rendering.
103+
* If {@code ignoreUndocumentedLinks} is {@code true}, undocumented links will be
104+
* ignored and will not trigger a failure.
105+
*
106+
* @param linkExtractor the link extractor
107+
* @param descriptors the link descriptors
108+
* @param attributes the additional attributes
109+
* @param ignoreUndocumentedLinks whether undocumented links should be ignored
110+
*/
111+
protected LinksSnippet(LinkExtractor linkExtractor, List<LinkDescriptor> descriptors,
112+
Map<String, Object> attributes, boolean ignoreUndocumentedLinks) {
76113
super("links", attributes);
77114
this.linkExtractor = linkExtractor;
78115
for (LinkDescriptor descriptor : descriptors) {
79116
Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel");
80117
this.descriptorsByRel.put(descriptor.getRel(), descriptor);
81118
}
119+
this.ignoreUndocumentedLinks = ignoreUndocumentedLinks;
82120
}
83121

84122
@Override
@@ -100,8 +138,14 @@ protected Map<String, Object> createModel(Operation operation) {
100138
private void validate(Map<String, List<Link>> links) {
101139
Set<String> actualRels = links.keySet();
102140

103-
Set<String> undocumentedRels = new HashSet<>(actualRels);
104-
undocumentedRels.removeAll(this.descriptorsByRel.keySet());
141+
Set<String> undocumentedRels;
142+
if (this.ignoreUndocumentedLinks) {
143+
undocumentedRels = Collections.emptySet();
144+
}
145+
else {
146+
undocumentedRels = new HashSet<>(actualRels);
147+
undocumentedRels.removeAll(this.descriptorsByRel.keySet());
148+
}
105149

106150
Set<String> requiredRels = new HashSet<>();
107151
for (Entry<String, LinkDescriptor> relAndDescriptor : this.descriptorsByRel

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,42 @@
3939
*/
4040
public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
4141

42-
private List<FieldDescriptor> fieldDescriptors;
42+
private final List<FieldDescriptor> fieldDescriptors;
43+
44+
private final boolean ignoreUndocumentedFields;
4345

4446
/**
4547
* Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named
4648
* {@code <type>-fields}. The fields will be documented using the given
4749
* {@code descriptors} and the given {@code attributes} will be included in the model
48-
* during template rendering.
50+
* during template rendering. Undocumented fields will trigger a failure.
4951
*
5052
* @param type the type of the fields
5153
* @param descriptors the field descriptors
5254
* @param attributes the additional attributes
55+
* @deprecated since 1.1 in favor of
56+
* {@link #AbstractFieldsSnippet(String, List, Map, boolean)}
5357
*/
58+
@Deprecated
5459
protected AbstractFieldsSnippet(String type, List<FieldDescriptor> descriptors,
5560
Map<String, Object> attributes) {
61+
this(type, descriptors, attributes, false);
62+
}
63+
64+
/**
65+
* Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named
66+
* {@code <type>-fields}. The fields will be documented using the given
67+
* {@code descriptors} and the given {@code attributes} will be included in the model
68+
* during template rendering. If {@code ignoreUndocumentedFields} is {@code true},
69+
* undocumented fields will be ignored and will not trigger a failure.
70+
*
71+
* @param type the type of the fields
72+
* @param descriptors the field descriptors
73+
* @param attributes the additional attributes
74+
* @param ignoreUndocumentedFields whether undocumented fields should be ignored
75+
*/
76+
protected AbstractFieldsSnippet(String type, List<FieldDescriptor> descriptors,
77+
Map<String, Object> attributes, boolean ignoreUndocumentedFields) {
5678
super(type + "-fields", attributes);
5779
for (FieldDescriptor descriptor : descriptors) {
5880
Assert.notNull(descriptor.getPath(), "Field descriptors must have a path");
@@ -65,6 +87,7 @@ protected AbstractFieldsSnippet(String type, List<FieldDescriptor> descriptors,
6587

6688
}
6789
this.fieldDescriptors = descriptors;
90+
this.ignoreUndocumentedFields = ignoreUndocumentedFields;
6891
}
6992

7093
@Override
@@ -111,8 +134,9 @@ private ContentHandler getContentHandler(Operation operation) {
111134
private void validateFieldDocumentation(ContentHandler payloadHandler) {
112135
List<FieldDescriptor> missingFields = payloadHandler
113136
.findMissingFields(this.fieldDescriptors);
114-
String undocumentedPayload = payloadHandler
115-
.getUndocumentedContent(this.fieldDescriptors);
137+
138+
String undocumentedPayload = this.ignoreUndocumentedFields ? null
139+
: payloadHandler.getUndocumentedContent(this.fieldDescriptors);
116140

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

0 commit comments

Comments
 (0)