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
This commit is contained in:
@@ -38,6 +38,10 @@ include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=links]
|
||||
The result is a snippet named `links.adoc` that contains a table describing the resource's
|
||||
links.
|
||||
|
||||
TIP: If a link in the response has a `title`, the description can be omitted from its
|
||||
descriptor and the `title` will be used. If you omit the description and the link does
|
||||
not have a `title` a failure will occur.
|
||||
|
||||
When documenting links, the test will fail if an undocumented link is found in the
|
||||
response. Similarly, the test will also fail if a documented link is not found in the
|
||||
response and the link has not been marked as optional.
|
||||
|
||||
@@ -51,7 +51,9 @@ class AtomLinkExtractor extends AbstractJsonLinkExtractor {
|
||||
Object hrefObject = linkMap.get("href");
|
||||
Object relObject = linkMap.get("rel");
|
||||
if (relObject instanceof String && hrefObject instanceof String) {
|
||||
return new Link((String) relObject, (String) hrefObject);
|
||||
Object titleObject = linkMap.get("title");
|
||||
return new Link((String) relObject, (String) hrefObject,
|
||||
titleObject instanceof String ? (String) titleObject : null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -67,9 +67,12 @@ class HalLinkExtractor extends AbstractJsonLinkExtractor {
|
||||
|
||||
private static Link maybeCreateLink(String rel, Object possibleLinkObject) {
|
||||
if (possibleLinkObject instanceof Map) {
|
||||
Object hrefObject = ((Map<?, ?>) possibleLinkObject).get("href");
|
||||
Map<?, ?> possibleLinkMap = (Map<?, ?>) possibleLinkObject;
|
||||
Object hrefObject = possibleLinkMap.get("href");
|
||||
if (hrefObject instanceof String) {
|
||||
return new Link(rel, (String) hrefObject);
|
||||
Object titleObject = possibleLinkMap.get("title");
|
||||
return new Link(rel, (String) hrefObject,
|
||||
titleObject instanceof String ? (String) titleObject : null);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -53,6 +53,10 @@ public abstract class HypermediaDocumentation {
|
||||
* If you do not want to document a link, a link descriptor can be marked as
|
||||
* {@link LinkDescriptor#ignored}. This will prevent it from appearing in the
|
||||
* generated snippet while avoiding the failure described above.
|
||||
* <p>
|
||||
* If a descriptor does not have a {@link LinkDescriptor#description(Object)
|
||||
* description}, the {@link Link#getTitle() title} of the link will be used. If the
|
||||
* link does not have a title a failure will occur.
|
||||
*
|
||||
* @param descriptors the descriptions of the response's links
|
||||
* @return the snippet that will document the links
|
||||
|
||||
@@ -29,6 +29,8 @@ public class Link {
|
||||
|
||||
private final String href;
|
||||
|
||||
private final String title;
|
||||
|
||||
/**
|
||||
* Creates a new {@code Link} with the given {@code rel} and {@code href}.
|
||||
*
|
||||
@@ -36,8 +38,21 @@ public class Link {
|
||||
* @param href The link's href
|
||||
*/
|
||||
public Link(String rel, String href) {
|
||||
this(rel, href, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code Link} with the given {@code rel}, {@code href}, and
|
||||
* {@code title}.
|
||||
*
|
||||
* @param rel The link's rel
|
||||
* @param href The link's href
|
||||
* @param title The link's title
|
||||
*/
|
||||
public Link(String rel, String href, String title) {
|
||||
this.rel = rel;
|
||||
this.href = href;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,12 +71,21 @@ public class Link {
|
||||
return this.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the link's {@code title}, or {@code null} if it does not have a title.
|
||||
* @return the link's {@code title} or {@code null}
|
||||
*/
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int prime = 31;
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + this.href.hashCode();
|
||||
result = prime * result + this.rel.hashCode();
|
||||
result = prime * result + ((this.title == null) ? 0 : this.title.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -83,13 +107,21 @@ public class Link {
|
||||
if (!this.rel.equals(other.rel)) {
|
||||
return false;
|
||||
}
|
||||
if (this.title == null) {
|
||||
if (other.title != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!this.title.equals(other.title)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this).append("rel", this.rel).append("href", this.href)
|
||||
.toString();
|
||||
.append("title", this.title).toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -77,12 +77,6 @@ public class LinksSnippet extends TemplatedSnippet {
|
||||
this.linkExtractor = linkExtractor;
|
||||
for (LinkDescriptor descriptor : descriptors) {
|
||||
Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel");
|
||||
if (!descriptor.isIgnored()) {
|
||||
Assert.notNull(descriptor.getDescription(),
|
||||
"The descriptor for link '" + descriptor.getRel()
|
||||
+ "' must either have a description or be" + " marked as "
|
||||
+ "ignored");
|
||||
}
|
||||
this.descriptorsByRel.put(descriptor.getRel(), descriptor);
|
||||
}
|
||||
}
|
||||
@@ -90,14 +84,16 @@ public class LinksSnippet extends TemplatedSnippet {
|
||||
@Override
|
||||
protected Map<String, Object> createModel(Operation operation) {
|
||||
OperationResponse response = operation.getResponse();
|
||||
Map<String, List<Link>> links;
|
||||
try {
|
||||
validate(this.linkExtractor.extractLinks(response));
|
||||
links = this.linkExtractor.extractLinks(response);
|
||||
validate(links);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ModelCreationException(ex);
|
||||
}
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("links", createLinksModel());
|
||||
model.put("links", createLinksModel(links));
|
||||
return model;
|
||||
}
|
||||
|
||||
@@ -135,17 +131,48 @@ public class LinksSnippet extends TemplatedSnippet {
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> createLinksModel() {
|
||||
private List<Map<String, Object>> createLinksModel(Map<String, List<Link>> links) {
|
||||
List<Map<String, Object>> model = new ArrayList<>();
|
||||
for (Entry<String, LinkDescriptor> entry : this.descriptorsByRel.entrySet()) {
|
||||
LinkDescriptor descriptor = entry.getValue();
|
||||
if (!descriptor.isIgnored()) {
|
||||
if (descriptor.getDescription() == null) {
|
||||
descriptor = createDescriptor(
|
||||
getDescriptionFromLinkTitle(links, descriptor.getRel()),
|
||||
descriptor);
|
||||
}
|
||||
model.add(createModelForDescriptor(descriptor));
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private String getDescriptionFromLinkTitle(Map<String, List<Link>> links,
|
||||
String rel) {
|
||||
List<Link> linksForRel = links.get(rel);
|
||||
if (linksForRel != null) {
|
||||
for (Link link : linksForRel) {
|
||||
if (link.getTitle() != null) {
|
||||
return link.getTitle();
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new SnippetException("No description was provided for the link with rel '"
|
||||
+ rel + "' and no title was available from the link in the payload");
|
||||
}
|
||||
|
||||
private LinkDescriptor createDescriptor(String description, LinkDescriptor source) {
|
||||
LinkDescriptor newDescriptor = new LinkDescriptor(source.getRel())
|
||||
.description(description);
|
||||
if (source.isOptional()) {
|
||||
newDescriptor.optional();
|
||||
}
|
||||
if (source.isIgnored()) {
|
||||
newDescriptor.ignored();
|
||||
}
|
||||
return newDescriptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code Map} of {@link LinkDescriptor LinkDescriptors} keyed by their
|
||||
* {@link LinkDescriptor#getRel() rels}.
|
||||
|
||||
@@ -68,14 +68,15 @@ public class LinkExtractorsPayloadTests {
|
||||
public void singleLink() throws IOException {
|
||||
Map<String, List<Link>> links = this.linkExtractor
|
||||
.extractLinks(createResponse("single-link"));
|
||||
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com")), links);
|
||||
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com", "Alpha")),
|
||||
links);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleLinksWithDifferentRels() throws IOException {
|
||||
Map<String, List<Link>> links = this.linkExtractor
|
||||
.extractLinks(createResponse("multiple-links-different-rels"));
|
||||
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com"),
|
||||
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com", "Alpha"),
|
||||
new Link("bravo", "http://bravo.example.com")), links);
|
||||
}
|
||||
|
||||
@@ -83,7 +84,8 @@ public class LinkExtractorsPayloadTests {
|
||||
public void multipleLinksWithSameRels() throws IOException {
|
||||
Map<String, List<Link>> links = this.linkExtractor
|
||||
.extractLinks(createResponse("multiple-links-same-rels"));
|
||||
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com/one"),
|
||||
assertLinks(Arrays.asList(
|
||||
new Link("alpha", "http://alpha.example.com/one", "Alpha one"),
|
||||
new Link("alpha", "http://alpha.example.com/two")), links);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,4 +79,16 @@ public class LinksSnippetFailureTests {
|
||||
this.snippet.getOutputDirectory()).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void linkWithNoDescription() throws IOException {
|
||||
this.thrown.expect(SnippetException.class);
|
||||
this.thrown.expectMessage(
|
||||
equalTo("No description was provided for the link with rel 'foo' and no"
|
||||
+ " title was available from the link in the payload"));
|
||||
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")),
|
||||
Arrays.asList(new LinkDescriptor("foo")))
|
||||
.document(new OperationBuilder("link-with-no-description",
|
||||
this.snippet.getOutputDirectory()).build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -87,6 +87,20 @@ public class LinksSnippetTests extends AbstractSnippetTests {
|
||||
.document(operationBuilder("documented-links").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void linkDescriptionFromTitleInPayload() throws IOException {
|
||||
this.snippet.expectLinks("link-description-from-title-in-payload")
|
||||
.withContents(tableWithHeader("Relation", "Description").row("a", "one")
|
||||
.row("b", "Link b"));
|
||||
new LinksSnippet(
|
||||
new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"),
|
||||
new Link("b", "bravo", "Link b")),
|
||||
Arrays.asList(new LinkDescriptor("a").description("one"),
|
||||
new LinkDescriptor("b"))).document(
|
||||
operationBuilder("link-description-from-title-in-payload")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void linksWithCustomAttributes() throws IOException {
|
||||
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"links": [ {
|
||||
"rel": "alpha",
|
||||
"href": "http://alpha.example.com"
|
||||
"href": "http://alpha.example.com",
|
||||
"title": "Alpha"
|
||||
}, {
|
||||
"rel": "bravo",
|
||||
"href": "http://bravo.example.com"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"links": [ {
|
||||
"rel": "alpha",
|
||||
"href": "http://alpha.example.com/one"
|
||||
"href": "http://alpha.example.com/one",
|
||||
"title": "Alpha one"
|
||||
}, {
|
||||
"rel": "alpha",
|
||||
"href": "http://alpha.example.com/two"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"links": [ {
|
||||
"rel": "alpha",
|
||||
"href": "http://alpha.example.com"
|
||||
"href": "http://alpha.example.com",
|
||||
"title": "Alpha"
|
||||
} ]
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"_links": {
|
||||
"alpha": {
|
||||
"href": "http://alpha.example.com"
|
||||
"href": "http://alpha.example.com",
|
||||
"title": "Alpha"
|
||||
},
|
||||
"bravo": {
|
||||
"href": "http://bravo.example.com"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"_links": {
|
||||
"alpha": [{
|
||||
"href": "http://alpha.example.com/one"
|
||||
"href": "http://alpha.example.com/one",
|
||||
"title": "Alpha one"
|
||||
}, {
|
||||
"href": "http://alpha.example.com/two"
|
||||
}]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"_links": {
|
||||
"alpha": {
|
||||
"href": "http://alpha.example.com"
|
||||
"href": "http://alpha.example.com",
|
||||
"title": "Alpha"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.restdocs.JUnitRestDocumentation;
|
||||
import org.springframework.restdocs.hypermedia.Link;
|
||||
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationIntegrationTests.TestConfiguration;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
@@ -461,7 +460,10 @@ public class MockMvcRestDocumentationIntegrationTests {
|
||||
public ResponseEntity<Map<String, Object>> foo() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("a", "alpha");
|
||||
response.put("links", Arrays.asList(new Link("rel", "href")));
|
||||
Map<String, String> link = new HashMap<>();
|
||||
link.put("rel", "rel");
|
||||
link.put("href", "href");
|
||||
response.put("links", Arrays.asList(link));
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("a", "alpha");
|
||||
return new ResponseEntity<>(response, headers, HttpStatus.OK);
|
||||
|
||||
@@ -40,7 +40,6 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.restdocs.JUnitRestDocumentation;
|
||||
import org.springframework.restdocs.hypermedia.Link;
|
||||
import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
@@ -329,7 +328,10 @@ public class RestAssuredRestDocumentationIntegrationTests {
|
||||
public ResponseEntity<Map<String, Object>> foo() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("a", "alpha");
|
||||
response.put("links", Arrays.asList(new Link("rel", "href")));
|
||||
Map<String, String> link = new HashMap<>();
|
||||
link.put("rel", "rel");
|
||||
link.put("href", "href");
|
||||
response.put("links", Arrays.asList(link));
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("a", "alpha");
|
||||
headers.add("Foo", "http://localhost:12345/foo/bar");
|
||||
|
||||
Reference in New Issue
Block a user