Initial commit

This commit is contained in:
Andy Wilkinson
2014-10-08 12:47:13 +01:00
commit b7904bcad7
64 changed files with 4937 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
= RESTful Notes User Guide
Andy Wilkinson;
:doctype: book
:toc:
:toclevels: 4
:source-highlighter: prettify
[introduction]
= Introduction
RESTful Notes is a RESTful web service for creating and storing notes. It uses hypermedia
to describe the relationships between resources and to allow navigation between them.
[getting-started]
= Getting started
[getting-started-running-the-service]
== Running the service
RESTful Notes is written using http://projects.spring.io/spring-boot[Spring Boot] which
makes it easy to get it up and running so that you can start exploring the REST API.
At the moment, binaries for RESTful Notes are not published anywhere. However, building
and running it from source is straightforward. The first step is to clone the Git
repository:
[source,bash]
----
$ git clone https://github.com/wilkinsona/spring-restdocs
----
Once the clone is complete, you're ready to get the service up and running:
[source,bash]
----
$ cd rest-notes
$ ./gradlew build
$ java -jar build/libs/*.jar
----
You can check that the service is up and running by executing a simple request using
cURL:
include::{generated}/index/access_with_curl_request.asciidoc[]
This request should yield the following response in the
http://stateless.co/hal_specification.html[Hypertext Application Language (HAL)] format:
include::{generated}/index/access_with_curl_response.asciidoc[]
Note the `_links` in the JSON response. They are key to navigating the API.
[getting-started-creating-a-note]
== Creating a note
Now that you've started the service and verified that it works, the next step is to use
it to create a new note. As you saw above, the URI for working with notes is included as
a link when you perform a `GET` request against the root of the service:
include::{generated}/index/access_with_curl_response.asciidoc[]
To create a note, you need to execute a `POST` request to this URI including a JSON
payload containing the title and body of the note:
include::{generated}/notes/create_with_curl_request.asciidoc[]
The response from this request should have a status code of `201 Created` and contain a
`Location` header whose value is the URI of the newly created note:
include::{generated}/notes/create_with_curl_response.asciidoc[]
To work with the newly created note you use the URI in the `Location` header. For example,
you can access the note's details by performing a `GET` request:
include::{generated}/notes/get_with_curl_request.asciidoc[]
This request will produce a response with the note's details in its body:
include::{generated}/notes/get_with_curl_response.asciidoc[]
Note the `tags` link which we'll make use of later.
[getting-started-creating-a-tag]
== Creating a tag
To make a note easier to find, it can be associated with any number of tags. To be able
to tag a note, you must first create the tag.
Referring back to the response for the service's index, the URI for working with tags is
include as a link:
include::{generated}/index/access_with_curl_response.asciidoc[]
To create a tag you need to execute a `POST` request to this URI, including a JSON
payload containing the name of the tag:
include::{generated}/tags/create_with_curl_request.asciidoc[]
The response from this request should have a status code of `201 Created` and contain a
`Location` header whose value is the URI of the newly created tag:
include::{generated}/tags/create_with_curl_response.asciidoc[]
To work with the newly created tag you use the URI in the `Location` header. For example
you can access the tag's details by performing a `GET` request:
include::{generated}/tags/get_with_curl_request.asciidoc[]
This request will produce a response with the tag's details in its body:
include::{generated}/tags/get_with_curl_response.asciidoc[]
[getting-started-tagging-a-note]
== Tagging a note
A tag isn't particularly useful until it's been associated with one or more notes. There
are two ways to tag a note: when the note is first created or by updating an existing
note. We'll look at both of these in turn.
[getting-started-tagging-a-note-creating]
=== Creating a tagged note
The process is largely the same as we saw before, but this time, in addition to providing
a title and body for the note, we'll also provide the tag that we want to be associated
with it.
Once again we execute a `POST` request. However, this time, in an array named tags, we
include the URI of the tag we just created:
include::{generated}/notes/create_tagged_with_curl_request.asciidoc[]
Once again, the response's `Location` header tells us the URI of the newly created note:
include::{generated}/notes/create_tagged_with_curl_response.asciidoc[]
As before, a `GET` request executed against this URI will retrieve the note's details:
include::{generated}/notes/get_tagged_with_curl.asciidoc[]
To verify that the tag has been associated with the note, we can perform a `GET` request
against the URI from the `tags` link:
include::{generated}/notes/get_tags_for_tagged_with_curl_request.asciidoc[]
The response embeds information about the tag that we've just associated with the note:
include::{generated}/notes/get_tags_for_tagged_with_curl_response.asciidoc[]
[getting-started-tagging-a-note-existing]
=== Tagging an existing note
An existing note can be tagged by executing a `PATCH` request against the note's URI with
a body that contains the array of tags to be associated with the note. We'll used the
URI of the untagged note that we created earlier:
include::{generated}/notes/tag_existing_with_curl_request.asciidoc[]
This request should produce a `204 No Content` response:
include::{generated}/notes/tag_existing_with_curl_response.asciidoc[]
When we first created this note, we noted the tags link included in its details:
include::{generated}/notes/get_with_curl_response.asciidoc[]
We can use that link now and execute a `GET` request to see that the note now has a
single tag:
include::{generated}/notes/get_tags_for_existing_with_curl.asciidoc[]

View File

@@ -0,0 +1,243 @@
/*
* 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.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.springframework.restdocs.core.RestDocumentationRequestPostProcessors.port;
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 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.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
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.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class GettingStartedDocumentation {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}
@Test
public void index() throws Exception {
this.mockMvc.perform(get("/").with(port(8080)).accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("_links.notes", is(notNullValue())))
.andExpect(jsonPath("_links.tags", is(notNullValue())))
.andDo(documentCurlRequest("index/access_with_curl_request.asciidoc"))
.andDo(documentCurlResponse("index/access_with_curl_response.asciidoc"));
}
@Test
public void creatingANote() throws JsonProcessingException, Exception {
String noteLocation = createNote();
getNote(noteLocation);
String tagLocation = createTag();
getTag(tagLocation);
String taggedNoteLocation = createTaggedNote(tagLocation);
getTaggedNote(taggedNoteLocation);
getTags(taggedNoteLocation);
tagExistingNote(noteLocation, tagLocation);
getTaggedExistingNote(noteLocation);
getTagsForExistingNote(noteLocation);
}
private String createNote() throws Exception {
Map<String, String> note = new HashMap<String, String>();
note.put("title", "Note creation with cURL");
note.put("body", "An example of how to create a note using cURL");
String noteLocation = this.mockMvc
.perform(
post("/notes").with(port(8080)).contentType(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", notNullValue()))
.andDo(documentCurlRequest("notes/create_with_curl_request.asciidoc")
.includeResponseHeaders())
.andDo(documentCurlResponse("notes/create_with_curl_response.asciidoc")
.includeResponseHeaders()).andReturn().getResponse()
.getHeader("Location");
return noteLocation;
}
private void getNote(String noteLocation) throws Exception {
this.mockMvc.perform(get(noteLocation).with(port(8080)))
.andExpect(status().isOk())
.andExpect(jsonPath("title", is(notNullValue())))
.andExpect(jsonPath("body", is(notNullValue())))
.andExpect(jsonPath("_links.tags", is(notNullValue())))
.andDo(documentCurlRequest("notes/get_with_curl_request.asciidoc"))
.andDo(documentCurlResponse("notes/get_with_curl_response.asciidoc"));
}
private String createTag() throws Exception, JsonProcessingException {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "getting-started");
String tagLocation = this.mockMvc
.perform(
post("/tags").with(port(8080)).contentType(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", notNullValue()))
.andDo(documentCurlRequest("tags/create_with_curl_request.asciidoc")
.includeResponseHeaders())
.andDo(documentCurlResponse("tags/create_with_curl_response.asciidoc")
.includeResponseHeaders()).andReturn().getResponse()
.getHeader("Location");
return tagLocation;
}
private void getTag(String tagLocation) throws Exception {
this.mockMvc.perform(get(tagLocation).with(port(8080)))
.andExpect(status().isOk())
.andExpect(jsonPath("name", is(notNullValue())))
.andExpect(jsonPath("_links.notes", is(notNullValue())))
.andDo(documentCurlRequest("tags/get_with_curl_request.asciidoc"))
.andDo(documentCurlResponse("tags/get_with_curl_response.asciidoc"));
}
private String createTaggedNote(String tag) throws Exception {
Map<String, Object> note = new HashMap<String, Object>();
note.put("title", "Tagged note creation with cURL");
note.put("body", "An example of how to create a tagged note using cURL");
note.put("tags", Arrays.asList(tag));
String noteLocation = this.mockMvc
.perform(
post("/notes").with(port(8080)).contentType(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", notNullValue()))
.andDo(documentCurlRequest(
"notes/create_tagged_with_curl_request.asciidoc")
.includeResponseHeaders())
.andDo(documentCurlResponse(
"notes/create_tagged_with_curl_response.asciidoc")
.includeResponseHeaders()).andReturn().getResponse()
.getHeader("Location");
return noteLocation;
}
private void getTaggedNote(String tagLocation) throws Exception {
this.mockMvc
.perform(get(tagLocation).with(port(8080)))
.andExpect(status().isOk())
.andExpect(jsonPath("title", is(notNullValue())))
.andExpect(jsonPath("body", is(notNullValue())))
.andExpect(jsonPath("_links.tags", is(notNullValue())))
.andDo(documentCurlRequestAndResponse("notes/get_tagged_with_curl.asciidoc"));
}
private void getTags(String taggedNoteLocation) throws Exception {
String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation))
.andReturn(), "tags");
this.mockMvc
.perform(get(tagsLocation).with(port(8080)))
.andExpect(status().isOk())
.andExpect(jsonPath("_embedded.tags", hasSize(1)))
.andDo(documentCurlRequest("notes/get_tags_for_tagged_with_curl_request.asciidoc"))
.andDo(documentCurlResponse("notes/get_tags_for_tagged_with_curl_response.asciidoc"));
}
private void tagExistingNote(String noteLocation, String tagLocation)
throws Exception {
Map<String, Object> update = new HashMap<String, Object>();
update.put("tags", Arrays.asList(tagLocation));
this.mockMvc
.perform(
patch(noteLocation).with(port(8080))
.contentType(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(update)))
.andExpect(status().isNoContent())
.andDo(documentCurlRequest(
"notes/tag_existing_with_curl_request.asciidoc")
.includeResponseHeaders())
.andDo(documentCurlResponse(
"notes/tag_existing_with_curl_response.asciidoc")
.includeResponseHeaders());
}
private void getTaggedExistingNote(String tagLocation) throws Exception {
this.mockMvc
.perform(get(tagLocation).with(port(8080)))
.andExpect(status().isOk())
.andDo(documentCurlRequestAndResponse("notes/get_tagged_existing_with_curl.asciidoc"));
}
private void getTagsForExistingNote(String taggedNoteLocation) throws Exception {
String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation))
.andReturn(), "tags");
this.mockMvc
.perform(get(tagsLocation).with(port(8080)))
.andExpect(status().isOk())
.andExpect(jsonPath("_embedded.tags", hasSize(1)))
.andDo(print())
.andDo(documentCurlRequestAndResponse("notes/get_tags_for_existing_with_curl.asciidoc"));
}
private String getLink(MvcResult result, String href)
throws UnsupportedEncodingException {
return JsonPath.parse(result.getResponse().getContentAsString()).read(
"_links.tags.href");
}
}

View File

@@ -0,0 +1 @@
org.springframework.restdocs.outputDir = build/generated-documentation

View File

@@ -0,0 +1,33 @@
/*
* 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 org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableAutoConfiguration
@ComponentScan
@EnableJpaRepositories
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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 java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
public class Note {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String title;
private String body;
@ManyToMany
private List<Tag> tags;
@JsonIgnore
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public List<Tag> getTags() {
return tags;
}
public void setTags(List<Tag> tags) {
this.tags = tags;
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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 org.springframework.data.repository.CrudRepository;
public interface NoteRepository extends CrudRepository<Note, Long> {
}

View File

@@ -0,0 +1,65 @@
/*
* 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 java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
@ManyToMany(mappedBy = "tags")
private List<Note> notes;
@JsonIgnore
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Note> getNotes() {
return notes;
}
public void setNotes(List<Note> notes) {
this.notes = notes;
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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 org.springframework.data.repository.CrudRepository;
public interface TagRepository extends CrudRepository<Tag, Long> {
}