Skip to content

Commit c4b7438

Browse files
committed
Derive a link's description from its title
Previously, a LinkDescriptor had to be created with both a rel and a description. If a description was not provided a failure would occur. This commit relaxes the above-described restriction by allowing a link's title to be used as its default description. If a descriptor has a description, it will always be used irrespective of whether or not the link has a title. If the descriptor does not have a description and the link does have a title, the link's title will be used. If the descriptor does not have a description and the link does not have a title a failure will occur. Closes gh-105
1 parent 984f90f commit c4b7438

File tree

17 files changed

+137
-27
lines changed

17 files changed

+137
-27
lines changed

docs/src/docs/asciidoc/documenting-your-api.adoc

+4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=links]
3838
The result is a snippet named `links.adoc` that contains a table describing the resource's
3939
links.
4040

41+
TIP: If a link in the response has a `title`, the description can be omitted from its
42+
descriptor and the `title` will be used. If you omit the description and the link does
43+
not have a `title` a failure will occur.
44+
4145
When documenting links, the test will fail if an undocumented link is found in the
4246
response. Similarly, the test will also fail if a documented link is not found in the
4347
response and the link has not been marked as optional.

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ private static Link maybeCreateLink(Map<String, Object> linkMap) {
5151
Object hrefObject = linkMap.get("href");
5252
Object relObject = linkMap.get("rel");
5353
if (relObject instanceof String && hrefObject instanceof String) {
54-
return new Link((String) relObject, (String) hrefObject);
54+
Object titleObject = linkMap.get("title");
55+
return new Link((String) relObject, (String) hrefObject,
56+
titleObject instanceof String ? (String) titleObject : null);
5557
}
5658
return null;
5759
}

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,12 @@ private static List<Link> convertToLinks(Object object, String rel) {
6767

6868
private static Link maybeCreateLink(String rel, Object possibleLinkObject) {
6969
if (possibleLinkObject instanceof Map) {
70-
Object hrefObject = ((Map<?, ?>) possibleLinkObject).get("href");
70+
Map<?, ?> possibleLinkMap = (Map<?, ?>) possibleLinkObject;
71+
Object hrefObject = possibleLinkMap.get("href");
7172
if (hrefObject instanceof String) {
72-
return new Link(rel, (String) hrefObject);
73+
Object titleObject = possibleLinkMap.get("title");
74+
return new Link(rel, (String) hrefObject,
75+
titleObject instanceof String ? (String) titleObject : null);
7376
}
7477
}
7578
return null;

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

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public static LinkDescriptor linkWithRel(String rel) {
5353
* If you do not want to document a link, a link descriptor can be marked as
5454
* {@link LinkDescriptor#ignored}. This will prevent it from appearing in the
5555
* generated snippet while avoiding the failure described above.
56+
* <p>
57+
* If a descriptor does not have a {@link LinkDescriptor#description(Object)
58+
* description}, the {@link Link#getTitle() title} of the link will be used. If the
59+
* link does not have a title a failure will occur.
5660
*
5761
* @param descriptors the descriptions of the response's links
5862
* @return the snippet that will document the links

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

+34-2
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,30 @@ public class Link {
2929

3030
private final String href;
3131

32+
private final String title;
33+
3234
/**
3335
* Creates a new {@code Link} with the given {@code rel} and {@code href}.
3436
*
3537
* @param rel The link's rel
3638
* @param href The link's href
3739
*/
3840
public Link(String rel, String href) {
41+
this(rel, href, null);
42+
}
43+
44+
/**
45+
* Creates a new {@code Link} with the given {@code rel}, {@code href}, and
46+
* {@code title}.
47+
*
48+
* @param rel The link's rel
49+
* @param href The link's href
50+
* @param title The link's title
51+
*/
52+
public Link(String rel, String href, String title) {
3953
this.rel = rel;
4054
this.href = href;
55+
this.title = title;
4156
}
4257

4358
/**
@@ -56,12 +71,21 @@ public String getHref() {
5671
return this.href;
5772
}
5873

74+
/**
75+
* Returns the link's {@code title}, or {@code null} if it does not have a title.
76+
* @return the link's {@code title} or {@code null}
77+
*/
78+
public String getTitle() {
79+
return this.title;
80+
}
81+
5982
@Override
6083
public int hashCode() {
61-
int prime = 31;
84+
final int prime = 31;
6285
int result = 1;
6386
result = prime * result + this.href.hashCode();
6487
result = prime * result + this.rel.hashCode();
88+
result = prime * result + ((this.title == null) ? 0 : this.title.hashCode());
6589
return result;
6690
}
6791

@@ -83,13 +107,21 @@ public boolean equals(Object obj) {
83107
if (!this.rel.equals(other.rel)) {
84108
return false;
85109
}
110+
if (this.title == null) {
111+
if (other.title != null) {
112+
return false;
113+
}
114+
}
115+
else if (!this.title.equals(other.title)) {
116+
return false;
117+
}
86118
return true;
87119
}
88120

89121
@Override
90122
public String toString() {
91123
return new ToStringCreator(this).append("rel", this.rel).append("href", this.href)
92-
.toString();
124+
.append("title", this.title).toString();
93125
}
94126

95127
}

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

+36-9
Original file line numberDiff line numberDiff line change
@@ -77,27 +77,23 @@ protected LinksSnippet(LinkExtractor linkExtractor, List<LinkDescriptor> descrip
7777
this.linkExtractor = linkExtractor;
7878
for (LinkDescriptor descriptor : descriptors) {
7979
Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel");
80-
if (!descriptor.isIgnored()) {
81-
Assert.notNull(descriptor.getDescription(),
82-
"The descriptor for link '" + descriptor.getRel()
83-
+ "' must either have a description or be" + " marked as "
84-
+ "ignored");
85-
}
8680
this.descriptorsByRel.put(descriptor.getRel(), descriptor);
8781
}
8882
}
8983

9084
@Override
9185
protected Map<String, Object> createModel(Operation operation) {
9286
OperationResponse response = operation.getResponse();
87+
Map<String, List<Link>> links;
9388
try {
94-
validate(this.linkExtractor.extractLinks(response));
89+
links = this.linkExtractor.extractLinks(response);
90+
validate(links);
9591
}
9692
catch (IOException ex) {
9793
throw new ModelCreationException(ex);
9894
}
9995
Map<String, Object> model = new HashMap<>();
100-
model.put("links", createLinksModel());
96+
model.put("links", createLinksModel(links));
10197
return model;
10298
}
10399

@@ -135,17 +131,48 @@ private void validate(Map<String, List<Link>> links) {
135131
}
136132
}
137133

138-
private List<Map<String, Object>> createLinksModel() {
134+
private List<Map<String, Object>> createLinksModel(Map<String, List<Link>> links) {
139135
List<Map<String, Object>> model = new ArrayList<>();
140136
for (Entry<String, LinkDescriptor> entry : this.descriptorsByRel.entrySet()) {
141137
LinkDescriptor descriptor = entry.getValue();
142138
if (!descriptor.isIgnored()) {
139+
if (descriptor.getDescription() == null) {
140+
descriptor = createDescriptor(
141+
getDescriptionFromLinkTitle(links, descriptor.getRel()),
142+
descriptor);
143+
}
143144
model.add(createModelForDescriptor(descriptor));
144145
}
145146
}
146147
return model;
147148
}
148149

150+
private String getDescriptionFromLinkTitle(Map<String, List<Link>> links,
151+
String rel) {
152+
List<Link> linksForRel = links.get(rel);
153+
if (linksForRel != null) {
154+
for (Link link : linksForRel) {
155+
if (link.getTitle() != null) {
156+
return link.getTitle();
157+
}
158+
}
159+
}
160+
throw new SnippetException("No description was provided for the link with rel '"
161+
+ rel + "' and no title was available from the link in the payload");
162+
}
163+
164+
private LinkDescriptor createDescriptor(String description, LinkDescriptor source) {
165+
LinkDescriptor newDescriptor = new LinkDescriptor(source.getRel())
166+
.description(description);
167+
if (source.isOptional()) {
168+
newDescriptor.optional();
169+
}
170+
if (source.isIgnored()) {
171+
newDescriptor.ignored();
172+
}
173+
return newDescriptor;
174+
}
175+
149176
/**
150177
* Returns a {@code Map} of {@link LinkDescriptor LinkDescriptors} keyed by their
151178
* {@link LinkDescriptor#getRel() rels}.

spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,24 @@ public LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType)
6868
public void singleLink() throws IOException {
6969
Map<String, List<Link>> links = this.linkExtractor
7070
.extractLinks(createResponse("single-link"));
71-
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com")), links);
71+
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com", "Alpha")),
72+
links);
7273
}
7374

7475
@Test
7576
public void multipleLinksWithDifferentRels() throws IOException {
7677
Map<String, List<Link>> links = this.linkExtractor
7778
.extractLinks(createResponse("multiple-links-different-rels"));
78-
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com"),
79+
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com", "Alpha"),
7980
new Link("bravo", "http://bravo.example.com")), links);
8081
}
8182

8283
@Test
8384
public void multipleLinksWithSameRels() throws IOException {
8485
Map<String, List<Link>> links = this.linkExtractor
8586
.extractLinks(createResponse("multiple-links-same-rels"));
86-
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com/one"),
87+
assertLinks(Arrays.asList(
88+
new Link("alpha", "http://alpha.example.com/one", "Alpha one"),
8789
new Link("alpha", "http://alpha.example.com/two")), links);
8890
}
8991

spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java

+12
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,16 @@ public void undocumentedLinkAndMissingLink() throws IOException {
7979
this.snippet.getOutputDirectory()).build());
8080
}
8181

82+
@Test
83+
public void linkWithNoDescription() throws IOException {
84+
this.thrown.expect(SnippetException.class);
85+
this.thrown.expectMessage(
86+
equalTo("No description was provided for the link with rel 'foo' and no"
87+
+ " title was available from the link in the payload"));
88+
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")),
89+
Arrays.asList(new LinkDescriptor("foo")))
90+
.document(new OperationBuilder("link-with-no-description",
91+
this.snippet.getOutputDirectory()).build());
92+
}
93+
8294
}

spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java

+14
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ public void documentedLinks() throws IOException {
8787
.document(operationBuilder("documented-links").build());
8888
}
8989

90+
@Test
91+
public void linkDescriptionFromTitleInPayload() throws IOException {
92+
this.snippet.expectLinks("link-description-from-title-in-payload")
93+
.withContents(tableWithHeader("Relation", "Description").row("a", "one")
94+
.row("b", "Link b"));
95+
new LinksSnippet(
96+
new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"),
97+
new Link("b", "bravo", "Link b")),
98+
Arrays.asList(new LinkDescriptor("a").description("one"),
99+
new LinkDescriptor("b"))).document(
100+
operationBuilder("link-description-from-title-in-payload")
101+
.build());
102+
}
103+
90104
@Test
91105
public void linksWithCustomAttributes() throws IOException {
92106
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);

spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"links": [ {
33
"rel": "alpha",
4-
"href": "http://alpha.example.com"
4+
"href": "http://alpha.example.com",
5+
"title": "Alpha"
56
}, {
67
"rel": "bravo",
78
"href": "http://bravo.example.com"

spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"links": [ {
33
"rel": "alpha",
4-
"href": "http://alpha.example.com/one"
4+
"href": "http://alpha.example.com/one",
5+
"title": "Alpha one"
56
}, {
67
"rel": "alpha",
78
"href": "http://alpha.example.com/two"
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"links": [ {
33
"rel": "alpha",
4-
"href": "http://alpha.example.com"
4+
"href": "http://alpha.example.com",
5+
"title": "Alpha"
56
} ]
67
}

spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"_links": {
33
"alpha": {
4-
"href": "http://alpha.example.com"
4+
"href": "http://alpha.example.com",
5+
"title": "Alpha"
56
},
67
"bravo": {
78
"href": "http://bravo.example.com"

spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"_links": {
33
"alpha": [{
4-
"href": "http://alpha.example.com/one"
4+
"href": "http://alpha.example.com/one",
5+
"title": "Alpha one"
56
}, {
67
"href": "http://alpha.example.com/two"
78
}]
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"_links": {
33
"alpha": {
4-
"href": "http://alpha.example.com"
4+
"href": "http://alpha.example.com",
5+
"title": "Alpha"
56
}
67
}
78
}

spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import org.springframework.http.MediaType;
3939
import org.springframework.http.ResponseEntity;
4040
import org.springframework.restdocs.JUnitRestDocumentation;
41-
import org.springframework.restdocs.hypermedia.Link;
4241
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationIntegrationTests.TestConfiguration;
4342
import org.springframework.test.context.ContextConfiguration;
4443
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -461,7 +460,10 @@ private static class TestController {
461460
public ResponseEntity<Map<String, Object>> foo() {
462461
Map<String, Object> response = new HashMap<>();
463462
response.put("a", "alpha");
464-
response.put("links", Arrays.asList(new Link("rel", "href")));
463+
Map<String, String> link = new HashMap<>();
464+
link.put("rel", "rel");
465+
link.put("href", "href");
466+
response.put("links", Arrays.asList(link));
465467
HttpHeaders headers = new HttpHeaders();
466468
headers.add("a", "alpha");
467469
return new ResponseEntity<>(response, headers, HttpStatus.OK);

spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import org.springframework.http.MediaType;
4141
import org.springframework.http.ResponseEntity;
4242
import org.springframework.restdocs.JUnitRestDocumentation;
43-
import org.springframework.restdocs.hypermedia.Link;
4443
import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication;
4544
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
4645
import org.springframework.test.context.web.WebAppConfiguration;
@@ -329,7 +328,10 @@ static class TestApplication {
329328
public ResponseEntity<Map<String, Object>> foo() {
330329
Map<String, Object> response = new HashMap<>();
331330
response.put("a", "alpha");
332-
response.put("links", Arrays.asList(new Link("rel", "href")));
331+
Map<String, String> link = new HashMap<>();
332+
link.put("rel", "rel");
333+
link.put("href", "href");
334+
response.put("links", Arrays.asList(link));
333335
HttpHeaders headers = new HttpHeaders();
334336
headers.add("a", "alpha");
335337
headers.add("Foo", "http://localhost:12345/foo/bar");

0 commit comments

Comments
 (0)