diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 9550aaf5..2dee3919 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java index ad12497a..71a2d8dc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java @@ -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; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java index eb55843a..866a8322 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java @@ -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; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index c2f11b75..f5493128 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -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. + *

+ * 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 diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java index 112886f0..01d5165c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java @@ -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(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 7c684ba9..528b444a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -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 createModel(Operation operation) { OperationResponse response = operation.getResponse(); + Map> links; try { - validate(this.linkExtractor.extractLinks(response)); + links = this.linkExtractor.extractLinks(response); + validate(links); } catch (IOException ex) { throw new ModelCreationException(ex); } Map 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> createLinksModel() { + private List> createLinksModel(Map> links) { List> model = new ArrayList<>(); for (Entry 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> links, + String rel) { + List 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}. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index b26374e8..fa578164 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -68,14 +68,15 @@ public class LinkExtractorsPayloadTests { public void singleLink() throws IOException { Map> 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> 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> 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); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java index 6ef2dfd8..217b753e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -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()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index b78b3b67..be122de6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -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); diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json index 35e63537..081c5d98 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json @@ -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" diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json index ff0d3f4a..28d76ac7 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json @@ -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" diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json index 57532675..d6339c03 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json @@ -1,6 +1,7 @@ { "links": [ { "rel": "alpha", - "href": "http://alpha.example.com" + "href": "http://alpha.example.com", + "title": "Alpha" } ] } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json index 08d0f3ea..6eecbf5c 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json @@ -1,7 +1,8 @@ { "_links": { "alpha": { - "href": "http://alpha.example.com" + "href": "http://alpha.example.com", + "title": "Alpha" }, "bravo": { "href": "http://bravo.example.com" diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json index 45ebca3c..9e5f51dc 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json @@ -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" }] diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json index d19ca125..43bf4273 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json @@ -1,7 +1,8 @@ { "_links": { "alpha": { - "href": "http://alpha.example.com" + "href": "http://alpha.example.com", + "title": "Alpha" } } } \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 51c35ee4..54bce09f 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -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> foo() { Map response = new HashMap<>(); response.put("a", "alpha"); - response.put("links", Arrays.asList(new Link("rel", "href"))); + Map 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); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 8d4da39a..408d2905 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -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> foo() { Map response = new HashMap<>(); response.put("a", "alpha"); - response.put("links", Arrays.asList(new Link("rel", "href"))); + Map 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");