Add support for automating the documentation of HAL Links
This commit is contained in:
@@ -12,6 +12,7 @@ project(':spring-restdocs-core') {
|
||||
compile "org.springframework:spring-test:$springVersion"
|
||||
compile "org.springframework:spring-web:$springVersion"
|
||||
compile 'javax.servlet:javax.servlet-api:3.1.0'
|
||||
compile 'com.fasterxml.jackson.core:jackson-databind:2.3.4'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ package com.example.notes;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.springframework.restdocs.core.RestDocumentation.document;
|
||||
import static org.springframework.restdocs.core.RestDocumentation.linkWithRel;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
@@ -94,10 +94,14 @@ public class ApiDocumentation {
|
||||
@Test
|
||||
public void indexExample() throws Exception {
|
||||
document("index-example",
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk()))
|
||||
.andExpect(jsonPath("_links.notes", is(notNullValue())))
|
||||
.andExpect(jsonPath("_links.tags", is(notNullValue())));
|
||||
this.mockMvc.perform(get("/")).andExpect(status().isOk()))
|
||||
.andDocumentHalLinks(
|
||||
linkWithRel("notes").description(
|
||||
"The <<resources-notes,Notes resource>>"),
|
||||
linkWithRel("tags").description(
|
||||
"The <<resources-tags,Tags resource>>"),
|
||||
linkWithRel("profile").description(
|
||||
"The ALPS profile for the service"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -168,7 +172,11 @@ public class ApiDocumentation {
|
||||
.andExpect(jsonPath("title", is(note.get("title"))))
|
||||
.andExpect(jsonPath("body", is(note.get("body"))))
|
||||
.andExpect(jsonPath("_links.self.href", is(noteLocation)))
|
||||
.andExpect(jsonPath("_links.tags", is(notNullValue())));
|
||||
.andExpect(jsonPath("_links.tags", is(notNullValue())))
|
||||
.andDocumentHalLinks(
|
||||
linkWithRel("self").description("This <<resources-note,note>>"),
|
||||
linkWithRel("tags").description(
|
||||
"This note's <<resources-note-tags,tags>>"));
|
||||
|
||||
}
|
||||
|
||||
@@ -253,8 +261,11 @@ public class ApiDocumentation {
|
||||
document("tag-get-example", this.mockMvc.perform(get(tagLocation)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("name", is(tag.get("name"))))
|
||||
.andExpect(jsonPath("_links.self.href", is(tagLocation)))
|
||||
.andExpect(jsonPath("_links.notes", is(notNullValue())));
|
||||
.andDocumentHalLinks(
|
||||
linkWithRel("self").description("This <<resources-tag,tag>>"),
|
||||
linkWithRel("notes")
|
||||
.description(
|
||||
"The <<resources-tagged-notes,notes>> that have this tag"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -132,15 +132,7 @@ include::{generated}/index-example/response.asciidoc[]
|
||||
[[resources-index-links]]
|
||||
==== Links
|
||||
|
||||
|===
|
||||
| Relation | Description
|
||||
|
||||
| notes
|
||||
| The <<resources-notes,Notes resource>>
|
||||
|
||||
| tags
|
||||
| The <<resources-tags,Tags resource>>
|
||||
|===
|
||||
include::{generated}/index-example/links.asciidoc[]
|
||||
|
||||
|
||||
|
||||
@@ -270,15 +262,7 @@ The Note resource is used to retrieve, update, and delete individual notes
|
||||
[[resources-note-links]]
|
||||
=== Links
|
||||
|
||||
|===
|
||||
| Relation | Description
|
||||
|
||||
| self
|
||||
| This <<resources-note,note>>
|
||||
|
||||
| note-tags
|
||||
| This note's <<resources-note-tags,tags>>
|
||||
|===
|
||||
include::{generated}/note-get-example/links.asciidoc[]
|
||||
|
||||
|
||||
|
||||
@@ -347,15 +331,7 @@ The Tag resource is used to retrieve, update, and delete individual tags
|
||||
[[resources-tag-links]]
|
||||
=== Links
|
||||
|
||||
|===
|
||||
| Relation | Description
|
||||
|
||||
| self
|
||||
| This <<resources-tag,tag>>
|
||||
|
||||
| tagged-notes
|
||||
| The <<resources-tagged-notes,notes>> that have this tag
|
||||
|===
|
||||
include::{generated}/tag-get-example/links.asciidoc[]
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.example.notes;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.springframework.restdocs.core.RestDocumentation.document;
|
||||
import static org.springframework.restdocs.core.RestDocumentation.linkWithRel;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
@@ -93,7 +94,12 @@ public class ApiDocumentation {
|
||||
@Test
|
||||
public void indexExample() throws Exception {
|
||||
document("index-example",
|
||||
this.mockMvc.perform(get("/")).andExpect(status().isOk()));
|
||||
this.mockMvc.perform(get("/")).andExpect(status().isOk()))
|
||||
.andDocumentHalLinks(
|
||||
linkWithRel("notes").description(
|
||||
"The <<resources-notes,Notes resource>>"),
|
||||
linkWithRel("tags").description(
|
||||
"The <<resources-tags,Tags resource>>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -164,7 +170,11 @@ public class ApiDocumentation {
|
||||
.andExpect(jsonPath("title", is(note.get("title"))))
|
||||
.andExpect(jsonPath("body", is(note.get("body"))))
|
||||
.andExpect(jsonPath("_links.self.href", is(noteLocation)))
|
||||
.andExpect(jsonPath("_links.note-tags", is(notNullValue())));
|
||||
.andExpect(jsonPath("_links.note-tags", is(notNullValue())))
|
||||
.andDocumentHalLinks(
|
||||
linkWithRel("self").description("This <<resources-note,note>>"),
|
||||
linkWithRel("note-tags").description(
|
||||
"This note's <<resources-note-tags,tags>>"));
|
||||
|
||||
}
|
||||
|
||||
@@ -249,8 +259,11 @@ public class ApiDocumentation {
|
||||
document("tag-get-example", this.mockMvc.perform(get(tagLocation)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("name", is(tag.get("name"))))
|
||||
.andExpect(jsonPath("_links.self.href", is(tagLocation)))
|
||||
.andExpect(jsonPath("_links.tagged-notes", is(notNullValue())));
|
||||
.andDocumentHalLinks(
|
||||
linkWithRel("self").description("This <<resources-tag,tag>>"),
|
||||
linkWithRel("tagged-notes")
|
||||
.description(
|
||||
"The <<resources-tagged-notes,notes>> that have this tag"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.restdocs.core;
|
||||
|
||||
public class LinkDescriptor {
|
||||
|
||||
private final String rel;
|
||||
|
||||
private String description;
|
||||
|
||||
public LinkDescriptor(String rel) {
|
||||
this.rel = rel;
|
||||
}
|
||||
|
||||
public LinkDescriptor description(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
String getRel() {
|
||||
return rel;
|
||||
}
|
||||
|
||||
String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -16,19 +16,23 @@
|
||||
|
||||
package org.springframework.restdocs.core;
|
||||
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequest;
|
||||
import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequestAndResponse;
|
||||
import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlResponse;
|
||||
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
public class RestDocumentation {
|
||||
|
||||
public static ResultActions document(String outputDir, ResultActions resultActions)
|
||||
throws Exception {
|
||||
return resultActions
|
||||
public static RestDocumentationResultActions document(String outputDir,
|
||||
ResultActions resultActions) throws Exception {
|
||||
return new RestDocumentationResultActions(outputDir, resultActions)
|
||||
.andDo(documentCurlRequest(outputDir).includeResponseHeaders())
|
||||
.andDo(documentCurlResponse(outputDir).includeResponseHeaders())
|
||||
.andDo(documentCurlRequestAndResponse(outputDir).includeResponseHeaders());
|
||||
}
|
||||
|
||||
public static LinkDescriptor linkWithRel(String rel) {
|
||||
return new LinkDescriptor(rel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.restdocs.core;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.restdocs.core.RestDocumentationResultHandlers.LinkDocumentingResultHandler;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.ResultHandler;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
|
||||
public class RestDocumentationResultActions implements ResultActions {
|
||||
|
||||
private final ResultActions delegate;
|
||||
|
||||
private final String outputDir;
|
||||
|
||||
public RestDocumentationResultActions(String outputDir, ResultActions delegate) {
|
||||
this.outputDir = outputDir;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestDocumentationResultActions andExpect(ResultMatcher matcher)
|
||||
throws Exception {
|
||||
this.delegate.andExpect(matcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestDocumentationResultActions andDo(ResultHandler handler) throws Exception {
|
||||
this.delegate.andDo(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MvcResult andReturn() {
|
||||
return this.delegate.andReturn();
|
||||
}
|
||||
|
||||
public RestDocumentationResultActions andDocumentHalLinks(LinkDescriptor... descriptors)
|
||||
throws Exception {
|
||||
this.delegate.andDo(new LinkDocumentingResultHandler(this.outputDir, Arrays
|
||||
.asList(descriptors)));
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.restdocs.core;
|
||||
|
||||
import static org.springframework.restdocs.core.IterableEnumeration.iterable;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -24,15 +25,24 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.io.StringWriter;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.restdocs.core.DocumentationWriter.DocumentationAction;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.ResultHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public abstract class RestDocumentationResultHandlers {
|
||||
|
||||
public static CurlResultHandler documentCurlRequest(String outputDir) {
|
||||
@@ -163,27 +173,19 @@ public abstract class RestDocumentationResultHandlers {
|
||||
|
||||
}
|
||||
|
||||
public static abstract class CurlResultHandler implements ResultHandler {
|
||||
|
||||
private final CurlConfiguration curlConfiguration = new CurlConfiguration();
|
||||
public static abstract class RestDocumentationResultHandler implements ResultHandler {
|
||||
|
||||
private String outputDir;
|
||||
|
||||
private String fileName;
|
||||
|
||||
public CurlResultHandler(String outputDir, String fileName) {
|
||||
|
||||
public RestDocumentationResultHandler(String outputDir, String fileName) {
|
||||
this.outputDir = outputDir;
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
CurlConfiguration getCurlConfiguration() {
|
||||
return this.curlConfiguration;
|
||||
}
|
||||
|
||||
public CurlResultHandler includeResponseHeaders() {
|
||||
this.curlConfiguration.includeResponseHeaders = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
abstract void handle(MvcResult result, DocumentationWriter writer)
|
||||
throws Exception;
|
||||
|
||||
@Override
|
||||
public void handle(MvcResult result) throws Exception {
|
||||
@@ -196,7 +198,7 @@ public abstract class RestDocumentationResultHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
private PrintStream createPrintStream()
|
||||
protected PrintStream createPrintStream()
|
||||
throws FileNotFoundException {
|
||||
|
||||
File outputFile = new File(this.outputDir, this.fileName + ".asciidoc");
|
||||
@@ -212,8 +214,80 @@ public abstract class RestDocumentationResultHandlers {
|
||||
return new File(new DocumentationProperties().getOutputDir(),
|
||||
outputFile.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class CurlResultHandler extends RestDocumentationResultHandler {
|
||||
|
||||
private final CurlConfiguration curlConfiguration = new CurlConfiguration();
|
||||
|
||||
public CurlResultHandler(String outputDir, String fileName) {
|
||||
super(outputDir, fileName);
|
||||
}
|
||||
|
||||
CurlConfiguration getCurlConfiguration() {
|
||||
return this.curlConfiguration;
|
||||
}
|
||||
|
||||
public CurlResultHandler includeResponseHeaders() {
|
||||
this.curlConfiguration.includeResponseHeaders = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
static class LinkDocumentingResultHandler extends RestDocumentationResultHandler {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private final Map<String, LinkDescriptor> descriptorsByRel = new HashMap<String, LinkDescriptor>();
|
||||
|
||||
public LinkDocumentingResultHandler(String outputDir, List<LinkDescriptor> descriptors) {
|
||||
super(outputDir, "links");
|
||||
for (LinkDescriptor descriptor: descriptors) {
|
||||
Assert.hasText(descriptor.getRel());
|
||||
Assert.hasText(descriptor.getDescription());
|
||||
this.descriptorsByRel.put(descriptor.getRel(), descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
void handle(MvcResult result, DocumentationWriter writer) throws Exception {
|
||||
Map<String, Object> json = this.objectMapper.readValue(result.getResponse().getContentAsString(), Map.class);
|
||||
Map<String, Object> links = (Map<String, Object>) json.get("_links");
|
||||
|
||||
Set<String> actualRels = links.keySet();
|
||||
Set<String> expectedRels = this.descriptorsByRel.keySet();
|
||||
|
||||
Set<String> undocumentedRels = new HashSet<String>(actualRels);
|
||||
undocumentedRels.removeAll(expectedRels);
|
||||
|
||||
Set<String> missingRels = new HashSet<String>(expectedRels);
|
||||
missingRels.removeAll(actualRels);
|
||||
|
||||
if (!undocumentedRels.isEmpty() || !missingRels.isEmpty()) {
|
||||
String message = "";
|
||||
if (!undocumentedRels.isEmpty()) {
|
||||
message += "Links with the following relations were not documented: " + undocumentedRels;
|
||||
}
|
||||
if (!missingRels.isEmpty()) {
|
||||
message += "Links with the following relations were not found in the response: " + missingRels;
|
||||
}
|
||||
fail(message);
|
||||
}
|
||||
|
||||
Assert.isTrue(actualRels.equals(expectedRels));
|
||||
|
||||
writer.println("|===");
|
||||
writer.println("| Relation | Description");
|
||||
|
||||
for (Entry<String, LinkDescriptor> entry : this.descriptorsByRel.entrySet()) {
|
||||
writer.println();
|
||||
writer.println("| " + entry.getKey());
|
||||
writer.println("| " + entry.getValue().getDescription());
|
||||
}
|
||||
|
||||
writer.println("|===");
|
||||
}
|
||||
|
||||
abstract void handle(MvcResult result, DocumentationWriter writer)
|
||||
throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user