Provide API guide examples alongside existing getting started guides

This commit is contained in:
Andy Wilkinson
2014-11-04 16:01:14 +00:00
parent 5168fcf93e
commit d06095ee72
12 changed files with 1487 additions and 12 deletions

View File

@@ -27,10 +27,11 @@ $ cd rest-notes-spring-data-rest
$ ./gradlew restDocumentation
```
Once the build is complete, open the generated documentation:
Once the build is complete, open on of the generated pieces HTML documentation:
```
open build/asciidoc/main.html
open build/asciidoc/getting-started-guide.html
open build/asciidoc/api-guide.html
```
Every example request and response in the documentation is auto-generated using custom

View File

@@ -0,0 +1,404 @@
= RESTful Notes API Guide
Andy Wilkinson;
:doctype: book
:toc:
:sectanchors:
:sectlinks:
:toclevels: 4
:source-highlighter: highlightjs
[[overview]]
= Overview
[[overview-http-verbs]]
== HTTP verbs
RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP verbs.
|===
| Verb | Usage
| `GET`
| Used to retrieve a resource
| `POST`
| Used to create a new resource
| `PATCH`
| Used to update an existing resource, including partial updates
| `DELETE`
| Used to delete an existing resource
|===
[[overview-http-status-codes]]
== HTTP status codes
RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP status codes.
|===
| Status code | Usage
| `200 OK`
| The request completed successfully
| `201 Created`
| A new resource has been created successfully. The resource's URI is available from the response's
`Location` header
| `204 No Content`
| An update to an existing resource has been applied successfully
| `400 Bad Request`
| The request was malformed. The response body will include an error providing further information
| `404 Not Found`
| The requested resource did not exist
|===
[[overview-errors]]
== Errors
Whenever an error response (status code >= 400) is returned, the body will contain a JSON object
that describes the problem. The error object has the following fields:
|===
| Field | Description
| error
| The HTTP error that occurred, e.g. `Bad Request`
| message
| A description of the cause of the error
| path
| The path to which the request was made
| status
| The HTTP status code, e.g. `400`
| timestamp
| The time, in milliseconds, at which the error occurred
|===
For example, a request that attempts to apply a non-existent tag to a note will produce a
`400 Bad Request` response:
include::{generated}/error-example/response.asciidoc[]
[[overview-hypermedia]]
== Hypermedia
RESTful Notes uses hypermedia and resources include links to other resources in their
responses. Responses are in http://stateless.co/hal_specification.html[Hypertext Application
Language (HAL)] format. Links can be found benath the `_links` key. Users of the API should
not created URIs themselves, instead they should use the above-described links to navigate
from resource to resource.
[[resources]]
= Resources
[[resources-index]]
== Index
The index provides the entry point into the service.
[[resources-index-access]]
=== Accessing the index
A `GET` request is used to access the index
==== Response structure
|===
| JSON path | Description
| `_links`
| <<resources-index-links,Links>> to other resources
|===
==== Example response
include::{generated}/index-example/response.asciidoc[]
[[resources-index-links]]
==== Links
|===
| Relation | Description
| notes
| The <<resources-notes,Notes resource>>
| tags
| The <<resources-tags,Tags resource>>
|===
[[resources-notes]]
== Notes
The Notes resources is used to create and list notes
[[resources-notes-list]]
=== Listing notes
A `GET` request will list all of the service's notes.
==== Response structure
|===
| JSON path | Description
| `_embedded.notes`
| An array of <<resources-note,Note resources>>
|===
==== Example request
include::{generated}/notes-list-example/request.asciidoc[]
==== Example response
include::{generated}/notes-list-example/response.asciidoc[]
[[resources-notes-create]]
=== Creating a note
A `POST` request is used to create a note
==== Request structure
|===
| JSON path | Description
| `title`
| The title of the note
| `body`
| The body of the note
| `tags`
| | The tags of the note as an array of URIs
|===
==== Example request
include::{generated}/notes-create-example/request.asciidoc[]
==== Example response
include::{generated}/notes-create-example/response.asciidoc[]
[[resources-tags]]
== Tags
The Tags resource is used to create and list tags.
[[resources-tags-list]]
=== Listing tags
A `GET` request will list all of the service's tags.
==== Response structure
|===
| JSON path | Description
| `_embedded.tags`
| An array of <<resources-tag,Tag resources>>
|===
==== Example request
include::{generated}/tags-list-example/request.asciidoc[]
==== Example response
include::{generated}/tags-list-example/response.asciidoc[]
[[resources-tags-create]]
=== Creating a tag
A `POST` request is used to create a note
==== Request structure
|===
| JSON path | Description
| `name`
| The name of the tag
|===
==== Example request
include::{generated}/tags-create-example/request.asciidoc[]
==== Example response
include::{generated}/tags-create-example/response.asciidoc[]
[[resources-note]]
== Note
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-tags,tags>>
|===
[[resources-note-retrieve]]
=== Retrieve a note
A `GET` request will retrieve the details of a note
Example response:
include::{generated}/note-get-example/response.asciidoc[]
|===
| JSON path | Description
| `title`
| The title of the note
| `body`
| The body of the note
| `_links`
| <<resources-note-links,Links>> to other resources
|===
[[resources-note-update]]
=== Update a note
A `PATCH` request is used to update a note
==== Request structure
|===
| JSON path | Description
| `title`
| The title of the note
| `body`
| The body of the note
| `tags`
| The tags of the note as an array of URIs
|===
To leave an attribute of a note unchanged, any of the above may be omitted from the request.
==== Example request
include::{generated}/note-update-example/request.asciidoc[]
==== Example response
include::{generated}/note-update-example/response.asciidoc[]
[[resources-note]]
== Tag
The Tag resource is used to retrieve, update, and delete individual tags
[[resources-tag-links]]
=== Links
|===
| Relation | Description
| self
| This <<resources-tag,tag>>
| notes
| The <<resources-notes,notes>> that have this tag
|===
[[resources-tag-retrieve]]
=== Retrieve a tag
A `GET` request will retrieve the details of a tag
Example response:
include::{generated}/tag-get-example/response.asciidoc[]
|===
| JSON path | Description
| `name`
| The name of the tag
| `_links`
| <<resources-tag-links,Links>> to other resources
|===
[[resources-tag-update]]
=== Update a tag
A `PATCH` request is used to update a tag
==== Request structure
|===
| JSON path | Description
| `name`
| The name of the tag
|===
==== Example request
include::{generated}/tag-update-example/request.asciidoc[]
==== Example response
include::{generated}/tag-update-example/response.asciidoc[]

View File

@@ -0,0 +1,297 @@
/*
* 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 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.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;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.hateoas.MediaTypes;
import org.springframework.restdocs.core.RestDocumentationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class)
@WebAppConfiguration
public class ApiDocumentation {
@Autowired
private NoteRepository noteRepository;
@Autowired
private TagRepository tagRepository;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfiguration()).build();
}
@Test
public void errorExample() throws Exception {
document(
"error-example",
this.mockMvc
.perform(get("/error")
.requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400)
.requestAttr(RequestDispatcher.ERROR_REQUEST_URI,
"/notes")
.requestAttr(RequestDispatcher.ERROR_MESSAGE,
"The tag 'http://localhost:8080/tags/123' does not exist")))
.andDo(print()).andExpect(status().isBadRequest())
.andExpect(jsonPath("error", is("Bad Request")))
.andExpect(jsonPath("timestamp", is(notNullValue())))
.andExpect(jsonPath("status", is(400)))
.andExpect(jsonPath("path", is(notNullValue())));
}
@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())));
}
@Test
public void notesListExample() throws Exception {
this.noteRepository.deleteAll();
createNote("REST maturity model",
"http://martinfowler.com/articles/richardsonMaturityModel.html");
createNote("Hypertext Application Language (HAL)",
"http://stateless.co/hal_specification.html");
createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/");
document("notes-list-example", this.mockMvc.perform(get("/notes"))).andExpect(
status().isOk());
}
@Test
public void notesCreateExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
String tagLocation = this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
Map<String, Object> note = new HashMap<String, Object>();
note.put("title", "REST maturity model");
note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html");
note.put("tags", Arrays.asList(tagLocation));
document(
"notes-create-example",
this.mockMvc.perform(
post("/notes").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(note))).andExpect(
status().isCreated()));
}
@Test
public void noteGetExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
String tagLocation = this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
Map<String, Object> note = new HashMap<String, Object>();
note.put("title", "REST maturity model");
note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html");
note.put("tags", Arrays.asList(tagLocation));
String noteLocation = this.mockMvc
.perform(
post("/notes").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
document("note-get-example", this.mockMvc.perform(get(noteLocation)))
.andExpect(status().isOk())
.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())));
}
@Test
public void tagsListExample() throws Exception {
this.noteRepository.deleteAll();
this.tagRepository.deleteAll();
createTag("REST");
createTag("Hypermedia");
createTag("HTTP");
document("tags-list-example", this.mockMvc.perform(get("/tags"))).andExpect(
status().isOk());
}
@Test
public void tagsCreateExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
document(
"tags-create-example",
this.mockMvc.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag))).andExpect(
status().isCreated()));
}
@Test
public void noteUpdateExample() throws Exception {
Map<String, Object> note = new HashMap<String, Object>();
note.put("title", "REST maturity model");
note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html");
String noteLocation = this.mockMvc
.perform(
post("/notes").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk())
.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())));
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
String tagLocation = this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
Map<String, Object> noteUpdate = new HashMap<String, Object>();
noteUpdate.put("tags", Arrays.asList(tagLocation));
document(
"note-update-example",
this.mockMvc.perform(
patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(noteUpdate)))
.andExpect(status().isNoContent()));
}
@Test
public void tagGetExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
String tagLocation = this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
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())));
}
@Test
public void tagUpdateExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
String tagLocation = this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
Map<String, Object> tagUpdate = new HashMap<String, Object>();
tagUpdate.put("name", "RESTful");
document(
"tag-update-example",
this.mockMvc.perform(
patch(tagLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tagUpdate)))
.andExpect(status().isNoContent()));
}
private void createNote(String title, String body) {
Note note = new Note();
note.setTitle(title);
note.setBody(body);
this.noteRepository.save(note);
}
private void createTag(String name) {
Tag tag = new Tag();
tag.setName(name);
this.tagRepository.save(tag);
}
}

View File

@@ -0,0 +1,404 @@
= RESTful Notes API Guide
Andy Wilkinson;
:doctype: book
:toc:
:sectanchors:
:sectlinks:
:toclevels: 4
:source-highlighter: highlightjs
[[overview]]
= Overview
[[overview-http-verbs]]
== HTTP verbs
RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP verbs.
|===
| Verb | Usage
| `GET`
| Used to retrieve a resource
| `POST`
| Used to create a new resource
| `PATCH`
| Used to update an existing resource, including partial updates
| `DELETE`
| Used to delete an existing resource
|===
[[overview-http-status-codes]]
== HTTP status codes
RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP status codes.
|===
| Status code | Usage
| `200 OK`
| The request completed successfully
| `201 Created`
| A new resource has been created successfully. The resource's URI is available from the response's
`Location` header
| `204 No Content`
| An update to an existing resource has been applied successfully
| `400 Bad Request`
| The request was malformed. The response body will include an error providing further information
| `404 Not Found`
| The requested resource did not exist
|===
[[overview-errors]]
== Errors
Whenever an error response (status code >= 400) is returned, the body will contain a JSON object
that describes the problem. The error object has the following fields:
|===
| Field | Description
| error
| The HTTP error that occurred, e.g. `Bad Request`
| message
| A description of the cause of the error
| path
| The path to which the request was made
| status
| The HTTP status code, e.g. `400`
| timestamp
| The time, in milliseconds, at which the error occurred
|===
For example, a request that attempts to apply a non-existent tag to a note will produce a
`400 Bad Request` response:
include::{generated}/error-example/response.asciidoc[]
[[overview-hypermedia]]
== Hypermedia
RESTful Notes uses hypermedia and resources include links to other resources in their
responses. Responses are in http://stateless.co/hal_specification.html[Hypertext Application
Language (HAL)] format. Links can be found benath the `_links` key. Users of the API should
not created URIs themselves, instead they should use the above-described links to navigate
from resource to resource.
[[resources]]
= Resources
[[resources-index]]
== Index
The index provides the entry point into the service.
[[resources-index-access]]
=== Accessing the index
A `GET` request is used to access the index
==== Response structure
|===
| JSON path | Description
| `_links`
| <<resources-index-links,Links>> to other resources
|===
==== Example response
include::{generated}/index-example/response.asciidoc[]
[[resources-index-links]]
==== Links
|===
| Relation | Description
| notes
| The <<resources-notes,Notes resource>>
| tags
| The <<resources-tags,Tags resource>>
|===
[[resources-notes]]
== Notes
The Notes resources is used to create and list notes
[[resources-notes-list]]
=== Listing notes
A `GET` request will list all of the service's notes.
==== Response structure
|===
| JSON path | Description
| `_embedded.notes`
| An array of <<resources-note,Note resources>>
|===
==== Example request
include::{generated}/notes-list-example/request.asciidoc[]
==== Example response
include::{generated}/notes-list-example/response.asciidoc[]
[[resources-notes-create]]
=== Creating a note
A `POST` request is used to create a note
==== Request structure
|===
| JSON path | Description
| `title`
| The title of the note
| `body`
| The body of the note
| `tags`
| | The tags of the note as an array of URIs
|===
==== Example request
include::{generated}/notes-create-example/request.asciidoc[]
==== Example response
include::{generated}/notes-create-example/response.asciidoc[]
[[resources-tags]]
== Tags
The Tags resource is used to create and list tags.
[[resources-tags-list]]
=== Listing tags
A `GET` request will list all of the service's tags.
==== Response structure
|===
| JSON path | Description
| `_embedded.tags`
| An array of <<resources-tag,Tag resources>>
|===
==== Example request
include::{generated}/tags-list-example/request.asciidoc[]
==== Example response
include::{generated}/tags-list-example/response.asciidoc[]
[[resources-tags-create]]
=== Creating a tag
A `POST` request is used to create a note
==== Request structure
|===
| JSON path | Description
| `name`
| The name of the tag
|===
==== Example request
include::{generated}/tags-create-example/request.asciidoc[]
==== Example response
include::{generated}/tags-create-example/response.asciidoc[]
[[resources-note]]
== Note
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>>
|===
[[resources-note-retrieve]]
=== Retrieve a note
A `GET` request will retrieve the details of a note
Example response:
include::{generated}/note-get-example/response.asciidoc[]
|===
| JSON path | Description
| `title`
| The title of the note
| `body`
| The body of the note
| `_links`
| <<resources-note-links,Links>> to other resources
|===
[[resources-note-update]]
=== Update a note
A `PATCH` request is used to update a note
==== Request structure
|===
| JSON path | Description
| `title`
| The title of the note
| `body`
| The body of the note
| `tags`
| The tags of the note as an array of URIs
|===
To leave an attribute of a note unchanged, any of the above may be omitted from the request.
==== Example request
include::{generated}/note-update-example/request.asciidoc[]
==== Example response
include::{generated}/note-update-example/response.asciidoc[]
[[resources-note]]
== Tag
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
|===
[[resources-tag-retrieve]]
=== Retrieve a tag
A `GET` request will retrieve the details of a tag
Example response:
include::{generated}/tag-get-example/response.asciidoc[]
|===
| JSON path | Description
| `name`
| The name of the tag
| `_links`
| <<resources-tag-links,Links>> to other resources
|===
[[resources-tag-update]]
=== Update a tag
A `PATCH` request is used to update a tag
==== Request structure
|===
| JSON path | Description
| `name`
| The name of the tag
|===
==== Example request
include::{generated}/tag-update-example/request.asciidoc[]
==== Example response
include::{generated}/tag-update-example/response.asciidoc[]

View File

@@ -1,4 +1,4 @@
= RESTful Notes User Guide
= RESTful Notes Getting Started Guide
Andy Wilkinson;
:doctype: book
:toc:

View File

@@ -0,0 +1,293 @@
/*
* 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 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.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;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.hateoas.MediaTypes;
import org.springframework.restdocs.core.RestDocumentationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class)
@WebAppConfiguration
public class ApiDocumentation {
@Autowired
private NoteRepository noteRepository;
@Autowired
private TagRepository tagRepository;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfiguration()).build();
}
@Test
public void errorExample() throws Exception {
document(
"error-example",
this.mockMvc
.perform(get("/error")
.requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400)
.requestAttr(RequestDispatcher.ERROR_REQUEST_URI,
"/notes")
.requestAttr(RequestDispatcher.ERROR_MESSAGE,
"The tag 'http://localhost:8080/tags/123' does not exist")))
.andDo(print()).andExpect(status().isBadRequest())
.andExpect(jsonPath("error", is("Bad Request")))
.andExpect(jsonPath("timestamp", is(notNullValue())))
.andExpect(jsonPath("status", is(400)))
.andExpect(jsonPath("path", is(notNullValue())));
}
@Test
public void indexExample() throws Exception {
document("index-example",
this.mockMvc.perform(get("/")).andExpect(status().isOk()));
}
@Test
public void notesListExample() throws Exception {
this.noteRepository.deleteAll();
createNote("REST maturity model",
"http://martinfowler.com/articles/richardsonMaturityModel.html");
createNote("Hypertext Application Language (HAL)",
"http://stateless.co/hal_specification.html");
createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/");
document("notes-list-example", this.mockMvc.perform(get("/notes"))).andExpect(
status().isOk());
}
@Test
public void notesCreateExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
String tagLocation = this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
Map<String, Object> note = new HashMap<String, Object>();
note.put("title", "REST maturity model");
note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html");
note.put("tags", Arrays.asList(tagLocation));
document(
"notes-create-example",
this.mockMvc.perform(
post("/notes").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(note))).andExpect(
status().isCreated()));
}
@Test
public void noteGetExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
String tagLocation = this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
Map<String, Object> note = new HashMap<String, Object>();
note.put("title", "REST maturity model");
note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html");
note.put("tags", Arrays.asList(tagLocation));
String noteLocation = this.mockMvc
.perform(
post("/notes").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
document("note-get-example", this.mockMvc.perform(get(noteLocation)))
.andExpect(status().isOk())
.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())));
}
@Test
public void tagsListExample() throws Exception {
this.noteRepository.deleteAll();
this.tagRepository.deleteAll();
createTag("REST");
createTag("Hypermedia");
createTag("HTTP");
document("tags-list-example", this.mockMvc.perform(get("/tags"))).andExpect(
status().isOk());
}
@Test
public void tagsCreateExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
document(
"tags-create-example",
this.mockMvc.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag))).andExpect(
status().isCreated()));
}
@Test
public void noteUpdateExample() throws Exception {
Map<String, Object> note = new HashMap<String, Object>();
note.put("title", "REST maturity model");
note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html");
String noteLocation = this.mockMvc
.perform(
post("/notes").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk())
.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())));
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
String tagLocation = this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
Map<String, Object> noteUpdate = new HashMap<String, Object>();
noteUpdate.put("tags", Arrays.asList(tagLocation));
document(
"note-update-example",
this.mockMvc.perform(
patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(noteUpdate)))
.andExpect(status().isNoContent()));
}
@Test
public void tagGetExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
String tagLocation = this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
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())));
}
@Test
public void tagUpdateExample() throws Exception {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "REST");
String tagLocation = this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()).andReturn().getResponse()
.getHeader("Location");
Map<String, Object> tagUpdate = new HashMap<String, Object>();
tagUpdate.put("name", "RESTful");
document(
"tag-update-example",
this.mockMvc.perform(
patch(tagLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tagUpdate)))
.andExpect(status().isNoContent()));
}
private void createNote(String title, String body) {
Note note = new Note();
note.setTitle(title);
note.setBody(body);
this.noteRepository.save(note);
}
private void createTag(String name) {
Tag tag = new Tag();
tag.setName(name);
this.tagRepository.save(tag);
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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 com.example.notes;
abstract class AbstractTagInput {
private final String name;
public AbstractTagInput(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View File

@@ -85,6 +85,11 @@ public class NotesController {
return httpHeaders;
}
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
void delete(@PathVariable("id") long id) {
this.noteRepository.delete(id);
}
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
Resource<Note> note(@PathVariable("id") long id) {
Note note = this.noteRepository.findById(id).orElseThrow(

View File

@@ -16,19 +16,15 @@
package com.example.notes;
import org.hibernate.validator.constraints.NotBlank;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class TagInput {
private final String name;
public class TagInput extends AbstractTagInput {
@JsonCreator
public TagInput(@JsonProperty("name") String name) {
this.name = name;
}
public String getName() {
return name;
public TagInput(@NotBlank @JsonProperty("name") String name) {
super(name);
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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 com.example.notes;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class TagPatchInput extends AbstractTagInput {
@JsonCreator
public TagPatchInput(@NullOrNotBlank @JsonProperty("name") String name) {
super(name);
}
}

View File

@@ -72,6 +72,11 @@ public class TagsController {
return httpHeaders;
}
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
void delete(@PathVariable("id") long id) {
this.repository.delete(id);
}
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
Resource<Tag> tag(@PathVariable("id") long id) {
Tag tag = this.repository.findById(id).orElseThrow(
@@ -86,4 +91,15 @@ public class TagsController {
.orElseThrow(() -> new ResourceDoesNotExistException())
.getNotes()));
}
@RequestMapping(value = "/{id}", method = RequestMethod.PATCH)
@ResponseStatus(HttpStatus.NO_CONTENT)
void updateTag(@PathVariable("id") long id, @RequestBody TagPatchInput tagInput) {
Tag tag = this.repository.findById(id).orElseThrow(
() -> new ResourceDoesNotExistException());
if (tagInput.getName() != null) {
tag.setName(tagInput.getName());
}
this.repository.save(tag);
}
}