Add Spring REST docs + various fixes

This commit is contained in:
Sebastien Deleuze
2016-03-19 17:01:12 +01:00
parent 08f9760af4
commit b52b7bab22
9 changed files with 418 additions and 37 deletions

View File

@@ -0,0 +1,180 @@
= Geospatial Messenger Getting Started Guide
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:
[introduction]
= Introduction
Geospatial Messenger provides a RESTful API for users and messages.
[[overview]]
= Overview
[[overview-http-verbs]]
== HTTP verbs
Person-service 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
| `PUT`
| Used to update an existing resource, full updates only
| `DELETE`
| Used to delete an existing resource
|===
[[overview-http-status-codes]]
== HTTP status codes
Person-service 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`
| Standard response for successful HTTP requests.
| The actual response will depend on the request method used.
| In a GET request, the response will contain an entity corresponding to the requested resource.
| In a POST request, the response will contain an entity describing or containing the result of the action.
| `201 Created`
| The request has been fulfilled and resulted in a new resource being created.
| `204 No Content`
| The server successfully processed the request, but is not returning any content.
| `400 Bad Request`
| The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
| `404 Not Found`
| The requested resource could not be found but may be available again in the future. Subsequent requests by the client are permissible.
|===
[[resources]]
= Resources
[[resources-user]]
== User
The User resource is used to create, modify and list users.
[[resource-user-list]]
[[resource-user-create]]
=== Creating user
A `POST` request creates a new user.
==== Example request
include::{snippets}/create-user/curl-request.adoc[]
==== Example response
include::{snippets}/create-user/http-response.adoc[]
[[resource-user-update-location]]
=== Updating user's location
This `PUT` request updates the user's location.
==== Example request
include::{snippets}/update-user-location/curl-request.adoc[]
==== Example response
include::{snippets}/update-user-location/http-response.adoc[]
[[resource-user-list]]
=== Listing users
A `GET` request lists all of the users.
include::{snippets}/list-users/response-fields.adoc[]
==== Example request
include::{snippets}/list-users/curl-request.adoc[]
==== Example response
include::{snippets}/list-users/http-response.adoc[]
[[resource-user-find-by-bounding-box]]
=== Find users by bounding box
This `GET` request lists all the users within the provided bounding box.
include::{snippets}/find-users-by-bounding-box/response-fields.adoc[]
==== Example request
include::{snippets}/find-users-by-bounding-box/curl-request.adoc[]
==== Example response
include::{snippets}/find-users-by-bounding-box/http-response.adoc[]
[[resources-message]]
== Message
The Message resource is used to create, modify and list messages.
[[resource-message-create]]
=== Creating message
A `POST` request creates a new message.
==== Example request
include::{snippets}/create-message/curl-request.adoc[]
==== Example response
include::{snippets}/create-message/http-response.adoc[]
[[resource-message-list]]
=== Listing messages
A `GET` request lists all of the messages.
include::{snippets}/list-messages/response-fields.adoc[]
==== Example request
include::{snippets}/list-messages/curl-request.adoc[]
==== Example response
include::{snippets}/list-messages/http-response.adoc[]
[[resource-message-find-by-bounding-box]]
=== Find messages by bounding box
This `GET` request lists all the messages within the provided bounding box.
include::{snippets}/find-messages-by-bounding-box/response-fields.adoc[]
==== Example request
include::{snippets}/find-messages-by-bounding-box/curl-request.adoc[]
==== Example response
include::{snippets}/find-messages-by-bounding-box/http-response.adoc[]
[[resource-message-subscribe]]
=== Receive new messages
A SSE endpoint that received all the new messages.
include::{snippets}/subscribe-messages/curl-request.adoc[]

View File

@@ -1,5 +1,7 @@
package io.spring.messenger
import com.fasterxml.jackson.annotation.JsonInclude.Include
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import io.spring.messenger.domain.Message
import io.spring.messenger.domain.User
@@ -17,8 +19,11 @@ import javax.sql.DataSource
@SpringBootApplication
open class Application {
@Bean open fun objectMapperBuilder(): Jackson2ObjectMapperBuilder
= Jackson2ObjectMapperBuilder().modulesToInstall(PostGISModule(), KotlinModule())
@Bean open fun objectMapper(): ObjectMapper {
val mapper:ObjectMapper = Jackson2ObjectMapperBuilder().modulesToInstall(PostGISModule(), KotlinModule()).build()
mapper.setSerializationInclusion(Include.NON_NULL)
return mapper
}
@Bean open fun db(dataSource: DataSource) = Database.connect(dataSource)

View File

@@ -17,8 +17,10 @@ open class UserRepository @Autowired constructor(val db: Database) {
create(Users)
}
open fun create(user: User) = db.transaction {
Users.insert( map(user) )
open fun create(user: User) {
db.transaction {
Users.insert( map(user) )
}
}
open fun updateLocation(userName:String, location: Point) = db.transaction {

View File

@@ -5,6 +5,7 @@ import io.spring.messenger.repository.MessageRepository
import org.postgis.PGbox2d
import org.postgis.Point
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus.*
import org.springframework.web.bind.annotation.*
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
@@ -14,14 +15,15 @@ class MessageController @Autowired constructor(val repository: MessageRepository
val broadcaster = SseBroadcaster()
@PostMapping
fun create(@RequestBody message: Message) {
repository.create(message)
broadcaster.send(message)
@PostMapping @ResponseStatus(CREATED)
fun create(@RequestBody message: Message): Message {
val m = repository.create(message)
broadcaster.send(m)
return m
}
@GetMapping
fun findMessages() = repository.findAll()
fun list() = repository.findAll()
@GetMapping("/bbox/{xMin},{yMin},{xMax},{yMax}")
fun findByBoundingBox(@PathVariable xMin:Double, @PathVariable yMin:Double,

View File

@@ -11,17 +11,18 @@ import org.springframework.web.bind.annotation.*
@RestController @RequestMapping("/user")
class UserController @Autowired constructor(val repository: UserRepository) {
@GetMapping fun findAll() = repository.findAll()
@PostMapping @ResponseStatus(CREATED)
fun create(@RequestBody u: User) { repository.create(u) }
@PostMapping fun create(@RequestBody u: User) = repository.create(u)
@PutMapping("/{userName}/location/{x},{y}") @ResponseStatus(NO_CONTENT)
fun updateLocation(@PathVariable userName:String, @PathVariable x: Double, @PathVariable y: Double)
= repository.updateLocation(userName, Point(x, y))
@GetMapping
fun list() = repository.findAll()
@GetMapping("/bbox/{xMin},{yMin},{xMax},{yMax}")
fun findByBoundingBox(@PathVariable xMin:Double, @PathVariable yMin:Double,
@PathVariable xMax:Double, @PathVariable yMax:Double)
= repository.findByBoundingBox(PGbox2d(Point(xMin, yMin), Point(xMax, yMax)))
@PutMapping("/{userName}/location/{x},{y}") @ResponseStatus(NO_CONTENT)
fun updateLocation(@PathVariable userName:String, @PathVariable x: Double, @PathVariable y: Double)
= repository.updateLocation(userName, Point(x, y))
}

View File

@@ -15,6 +15,9 @@
<div>
<span>Current user: </span><select id="select-user"></select>
</div>
<div>
<a href="/docs/index.html">REST API documentation</a>
</div>
<script src="map.js"></script>
</body>
</html>

View File

@@ -1,40 +1,125 @@
package io.spring.messenger
import com.fasterxml.jackson.databind.ObjectMapper
import io.spring.messenger.domain.Message
import io.spring.messenger.domain.User
import io.spring.messenger.repository.MessageRepository
import io.spring.messenger.repository.UserRepository
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.postgis.Point
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.SpringApplicationConfiguration
import org.springframework.http.MediaType.APPLICATION_JSON_UTF8
import org.springframework.restdocs.RestDocumentation
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler
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.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup
import org.springframework.web.context.WebApplicationContext
@RunWith(SpringJUnit4ClassRunner::class)
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration
import org.springframework.restdocs.operation.preprocess.Preprocessors.*
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.restdocs.payload.PayloadDocumentation.*
import org.springframework.restdocs.request.RequestDocumentation.*
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*
@RunWith(SpringJUnit4ClassRunner::class) @WebAppConfiguration
@SpringApplicationConfiguration(classes = arrayOf(Application::class))
@WebAppConfiguration
class MessageControllerTests {
@Rule @JvmField val restDoc = RestDocumentation("build/generated-snippets")
@Autowired lateinit var context: WebApplicationContext
@Autowired lateinit var userRepository: UserRepository
@Autowired lateinit var messageRepository: MessageRepository
@Autowired lateinit var userRepository: UserRepository
@Autowired lateinit var mapper: ObjectMapper
lateinit var mockMvc: MockMvc
lateinit var document: RestDocumentationResultHandler
@Before fun setUp() {
messageRepository.deleteAll()
userRepository.deleteAll()
mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build()
document = document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()));
mockMvc = webAppContextSetup(this.context)
.apply<DefaultMockMvcBuilder>(documentationConfiguration(restDoc))
.alwaysDo<DefaultMockMvcBuilder>(document).build()
}
@Test fun findUsers() {
mockMvc.perform(get("/message").accept(APPLICATION_JSON)).andExpect(status().isOk)
@Test fun listMessages() {
userRepository.create(User("swhite", "Skyler", "White"))
messageRepository.create(Message("foo", "swhite"))
messageRepository.create(Message("bar", "swhite", Point(0.0, 0.0)))
document.snippets(
responseFields(
fieldWithPath("[].id").description("The message ID"),
fieldWithPath("[].content").description("The message content"),
fieldWithPath("[].author").description("The message author username"),
fieldWithPath("[].location").optional().description("Optional, the message location (latitude, longitude)")
)
)
mockMvc.perform(get("/message").accept(APPLICATION_JSON_UTF8)).andExpect(status().isOk)
}
@Test fun createMessage() {
userRepository.create(User("swhite", "Skyler", "White"))
document.snippets(
requestFields(
fieldWithPath("content").description("The message content"),
fieldWithPath("author").description("The message author username"),
fieldWithPath("location").optional().description("Optional, the message location (latitude, longitude)")
),
responseFields(
fieldWithPath("id").description("The message ID"),
fieldWithPath("content").description("The message content"),
fieldWithPath("author").description("The message author username"),
fieldWithPath("location").optional().description("Optional, the message location (latitude, longitude)")
)
)
var message = Message("""Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum."""
, "swhite", Point(0.0, 0.0))
mockMvc.perform(post("/message")
.content(mapper.writeValueAsString(message))
.contentType(APPLICATION_JSON_UTF8))
.andExpect(status().isCreated)
}
@Test fun findMessagesByBoundingBox() {
userRepository.create(User("swhite", "Skyler", "White"))
messageRepository.create(Message("foo", "swhite", Point(0.0, 0.0)))
messageRepository.create(Message("bar", "swhite", Point(1.0, 1.0)))
document.snippets(
pathParameters(
parameterWithName("xMin").description("The latitude of the lower-left corner"),
parameterWithName("yMin").description("The longitude of the lower-left corner"),
parameterWithName("xMax").description("The latitude of the upper-left corner"),
parameterWithName("yMax").description("The longitude of the upper-left corner")
),
responseFields(
fieldWithPath("[].id").description("The message ID"),
fieldWithPath("[].content").description("The message content"),
fieldWithPath("[].author").description("The message author username"),
fieldWithPath("[].location").optional().description("Optional, the message location (latitude, longitude)")
)
)
mockMvc.perform(get("/message/bbox/{xMin},{yMin},{xMax},{yMax}", -1, -1, 2, 2)
.accept(APPLICATION_JSON_UTF8))
.andExpect(status().isOk)
}
@Test fun subscribeMessages() {
mockMvc.perform(get("/message/subscribe")).andExpect(status().isOk)
}
}

View File

@@ -1,40 +1,122 @@
package io.spring.messenger
import com.fasterxml.jackson.databind.ObjectMapper
import io.spring.messenger.domain.User
import io.spring.messenger.repository.MessageRepository
import io.spring.messenger.repository.UserRepository
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.postgis.Point
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.SpringApplicationConfiguration
import org.springframework.http.MediaType.APPLICATION_JSON_UTF8
import org.springframework.restdocs.RestDocumentation
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.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup
import org.springframework.web.context.WebApplicationContext
@RunWith(SpringJUnit4ClassRunner::class)
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler
import org.springframework.restdocs.operation.preprocess.Preprocessors.*
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.restdocs.payload.PayloadDocumentation.*
import org.springframework.restdocs.request.RequestDocumentation.*
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*
@RunWith(SpringJUnit4ClassRunner::class) @WebAppConfiguration
@SpringApplicationConfiguration(classes = arrayOf(Application::class))
@WebAppConfiguration
class UserControllerTests {
@Rule @JvmField val restDoc = RestDocumentation("build/generated-snippets")
@Autowired lateinit var context: WebApplicationContext
@Autowired lateinit var userRepository: UserRepository
@Autowired lateinit var messageRepository: MessageRepository
@Autowired lateinit var mapper: ObjectMapper
lateinit var mockMvc: MockMvc
lateinit var document: RestDocumentationResultHandler
@Before fun setUp() {
messageRepository.deleteAll()
userRepository.deleteAll()
mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build()
document = document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()));
mockMvc = webAppContextSetup(this.context)
.apply<DefaultMockMvcBuilder>(documentationConfiguration(restDoc))
.alwaysDo<DefaultMockMvcBuilder>(document).build()
}
@Test fun findUsers() {
mockMvc.perform(get("/user").accept(APPLICATION_JSON)).andExpect(status().isOk)
@Test fun createUser() {
document.snippets(
requestFields(
fieldWithPath("userName").description("The user username"),
fieldWithPath("firstName").description("The user first name"),
fieldWithPath("lastName").description("The user last name"),
fieldWithPath("location").optional().description("Optional, the user location (latitude, longitude)")
)
)
var skyler = User("swhite", "Skyler", "White", Point(0.0, 0.0))
mockMvc.perform(post("/user")
.content(mapper.writeValueAsString(skyler))
.contentType(APPLICATION_JSON_UTF8)
.accept(APPLICATION_JSON_UTF8))
.andExpect(status().isCreated)
}
@Test fun listUsers() {
userRepository.create(User("swhite", "Skyler", "White"))
userRepository.create(User("jpinkman", "Jesse", "Pinkman", Point(0.0, 0.0)))
document.snippets(
responseFields(
fieldWithPath("[].userName").description("The user username"),
fieldWithPath("[].firstName").description("The user first name"),
fieldWithPath("[].lastName").description("The user last name"),
fieldWithPath("[].location").optional().description("Optional, the user location (latitude, longitude)")
)
)
mockMvc.perform(get("/user").accept(APPLICATION_JSON_UTF8)).andExpect(status().isOk)
}
@Test fun findUsersByBoundingBox() {
userRepository.create(User("swhite", "Skyler", "White", Point(0.0, 0.0)))
userRepository.create(User("jpinkman", "Jesse", "Pinkman", Point(1.0, 1.0)))
document.snippets(
pathParameters(
parameterWithName("xMin").description("The latitude of the lower-left corner"),
parameterWithName("yMin").description("The longitude of the lower-left corner"),
parameterWithName("xMax").description("The latitude of the upper-left corner"),
parameterWithName("yMax").description("The longitude of the upper-left corner")
),
responseFields(
fieldWithPath("[].userName").description("The user username"),
fieldWithPath("[].firstName").description("The user first name"),
fieldWithPath("[].lastName").description("The user last name"),
fieldWithPath("[].location").optional().description("Optional, the user location (latitude, longitude)")
)
)
mockMvc.perform(get("/user/bbox/{xMin},{yMin},{xMax},{yMax}", -1, -1, 2, 2)
.accept(APPLICATION_JSON_UTF8))
.andExpect(status().isOk)
}
@Test fun updateUserLocation() {
userRepository.create(User("swhite", "Skyler", "White", Point(0.0, 0.0)))
document.snippets(
pathParameters(
parameterWithName("userName").description("The user username"),
parameterWithName("x").description("The new location latitude"),
parameterWithName("y").description("The new location latitude")
)
)
mockMvc.perform(put("/user/{userName}/location/{x},{y}", "swhite", 1.0, 1.0))
.andExpect(status().isNoContent)
}
}