Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,33 @@ buildscript {
}
}

plugins {
id "org.asciidoctor.convert" version "1.5.2"
}

ext {
snippetsDir = file('build/generated-snippets')
}

repositories {
mavenLocal()
mavenCentral()
}

apply plugin: "idea"
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'org.springframework.boot'
apply plugin: "io.spring.dependency-management"
apply plugin: "org.flywaydb.flyway"

repositories {
mavenLocal()
mavenCentral()
}

jar {
baseName = "startapp"
dependsOn asciidoctor
from("${asciidoctor.outputDir}/html5") {
into "static/docs"
}
}

sourceCompatibility = 1.8
Expand All @@ -56,13 +69,24 @@ dependencies {
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
compile "org.springframework.boot:spring-boot-starter-hateoas"
runtime "org.springframework.boot:spring-boot-devtools"
//utils
compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.8.4"
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"
//data
compile "org.springframework.boot:spring-boot-starter-jooq"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "org.flywaydb:flyway-core"
runtime "com.h2database:h2"
//rest
compile "org.springframework.data:spring-data-rest-hal-browser"

// Initial test dependencies.
testCompile("org.springframework.boot:spring-boot-starter-test")
testCompile("junit:junit:4.12")

// Extras
testCompile("com.jayway.jsonpath:json-path:2.0.0")
testCompile("org.springframework.restdocs:spring-restdocs-mockmvc:1.1.2.RELEASE")
}

task generateDbShemaSource << {
Expand Down Expand Up @@ -107,6 +131,18 @@ flyway {
locations = ["filesystem:$project.projectDir/src/main/resources/db/migration"]
}


test {
outputs.dir snippetsDir
}

asciidoctor {
attributes 'snippets': snippetsDir
inputs.dir snippetsDir
dependsOn test
dependsOn test
}

task wrapper(type: Wrapper) {
gradleVersion = "2.14.1"
}
Expand Down
107 changes: 107 additions & 0 deletions src/docs/asciidoc/api-guide.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
= Maxur Tutor - API Guide
Maxim Yunusov;
:doctype: book
:toc:
:sectanchors:
:sectlinks:
:toclevels: 4
:source-highlighter: highlightjs

[[overview]]
= Overview

[[overview-http-verbs]]
== HTTP verbs

|===
| Verb | Usage

| `GET`
| Used to retrieve a resource

| `POST`
| Used to create a new resource

| `PUT`
| Used to create a new resource as well as replace existing 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

|===
| 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

| `405 Method Not Allowed`
| The requested resource does not support method

| `409 Conflict`
| The request tries to put the resource into a conflicting state
|===

[[overview-hypermedia]]
== Hypermedia

This API 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 beneath the `_links` key. Users of the API should
not create URIs themselves, instead they should use the above-described links to navigate
from resource to resource.

[[resources]]
= Resources

[[issues]]
== Issues

The Ads resource / 'issues' relation is used to create and list issues.

[[resources-issues-create]]

=== Creating an issue

A `POST` request is used to create an issue

==== Example curl request

include::{snippets}/create-issue/curl-request.adoc[]

==== Example HTTP request

include::{snippets}/create-issue/http-request.adoc[]

==== Example response

include::{snippets}/create-issue/http-response.adoc[]

==== Links

include::{snippets}/create-issue/links.adoc[]

==== Response Fields

include::{snippets}/create-issue/response-fields.adoc[]


Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import org.maxur.tutor.startapp.domain.IssueRepository
import org.springframework.hateoas.ExposesResourceFor
import org.springframework.hateoas.ResourceSupport
import org.springframework.hateoas.mvc.ControllerLinkBuilder
import org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.*

/**
* Project Resource Controller
Expand All @@ -31,6 +31,18 @@ class IssueController(val repository: IssueRepository) {
return ResponseEntity(full(issue), HttpStatus.OK)
}

@ResponseBody
@PostMapping("", produces = arrayOf("application/hal+json"), consumes = arrayOf("application/json"))
fun add(@RequestBody issue: Issue): ResponseEntity<FullIssueResource>{
repository.add(issue)
val headers: HttpHeaders = HttpHeaders()
headers.add(
HttpHeaders.LOCATION,
linkTo(IssueController::class.java).slash(issue.id).withSelfRel().expand(issue.id).href
)
return ResponseEntity(full(issue), headers, HttpStatus.CREATED)
}

private fun full(issue: Issue): FullIssueResource {
val link = ControllerLinkBuilder.linkTo(IssueController::class.java)
.slash(issue.id)
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/org/maxur/tutor/startapp/domain/Issue.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ package org.maxur.tutor.startapp.domain
* @version 1.0
* @since <pre>18.01.2017</pre>
*/
data class Issue(val id: String, val name: String, val description: String?)
data class Issue(val id: String, val name: String, val description: String?, val projectId: String)
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ open class IssueRepository(val dsl: DSLContext) {
fun findOne(id: String): Issue? =
select.where(ISSUES.ISSUE_ID.eq(id))
.fetchOneInto(Issues::class.java)
?.let { record -> Issue(record.issueId, record.name, record.description)}
?.let { record -> Issue(record.issueId, record.name, record.description, record.projectId)}

fun add(issue: Issue) {
dsl.insertInto(ISSUES, ISSUES.ISSUE_ID, ISSUES.NAME, ISSUES.DESCRIPTION, ISSUES.PROJECT_ID)
.values(issue.id, issue.name, issue.description, issue.projectId)
.execute()

}


}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ open class ProjectRepository(val dsl: DSLContext) {
.sortAsc(PROJECTS.PROJECT_ID)
.into(ISSUES.ISSUE_ID, ISSUES.NAME, ISSUES.DESCRIPTION)
.filter { iss -> iss.value1() != null }
.map { iss -> Issue(iss.value1(), iss.value2(), iss.value3()) }
.map { iss -> Issue(iss.value1(), iss.value2(), iss.value3(), project.value1()) }
.toList()
.orEmpty()
return Project(project.value1(), project.value2(), issues)
Expand Down
27 changes: 27 additions & 0 deletions src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
logging:
level:
org.jooq.tools: debug

flyway:
enabled: true
schemas: PUBLIC


spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:alm;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
schema: PUBLIC

h2.console:
enabled: true
path: /admin/h2

jooq:
sql-dialect: h2

management:
context-path: /admin

1 change: 1 addition & 0 deletions src/main/resources/documentatiomproperties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.springframework.restdocs.outputDir: build/generated-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.maxur.tutor.startapp

import org.hamcrest.Matchers
import org.junit.Test
import org.junit.runner.RunWith
import org.maxur.tutor.startapp.domain.Issue
import org.maxur.tutor.startapp.domain.IssueRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.hateoas.MediaTypes
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.restdocs.hypermedia.HypermediaDocumentation.*
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
import org.springframework.restdocs.payload.JsonFieldType
import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath
import org.springframework.restdocs.payload.PayloadDocumentation.responseFields
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.junit4.SpringRunner
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*

@RunWith(SpringRunner::class)
@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureMockMvc
@AutoConfigureRestDocs("build/generated-snippets")
class IssueHttpApiWithDocsTests {

@Autowired
lateinit var mockMvc: MockMvc

@Autowired
lateinit var repository: IssueRepository

@Test
@Throws(Exception::class)
fun createIssue() {
val issue = issue()
val requestBody = saveRequestJsonString(issue)

val resultActions = mockMvc.perform(MockMvcRequestBuilders
.post("/issues")
.accept(MediaTypes.HAL_JSON)
.content(requestBody)
.contentType(MediaType.APPLICATION_JSON)
)

resultActions.andExpect(status().isCreated)

val createdIssue = findCreatedIssue() ?: throw AssertionError()

resultActions
.andExpect(header().string(HttpHeaders.LOCATION, "http://localhost:8080/issues/" + createdIssue.id))
.andExpect(jsonPath("$.name", Matchers.`is`(createdIssue.name)))
.andExpect(jsonPath("$.description", Matchers.`is`(createdIssue.description)))

resultActions.andDo(document("create-issue",
links(halLinks(),
linkWithRel("self").description("This issue")
),
responseFields(
fieldWithPath("_links").type(JsonFieldType.OBJECT).description("Links"),
fieldWithPath("name").type(JsonFieldType.STRING).description("Issue name"),
fieldWithPath("description").type(JsonFieldType.STRING).description("Issue description")
)))
}

private fun findCreatedIssue(): Issue? {
return repository.findOne("id")
}

private fun issue(): Issue {
return Issue("id", "Issue", "Description", "pr1")
}

private fun saveRequestJsonString(issue: Issue): String = """{
"id": "${issue.id}",
"name": "${issue.name}",
"description": "${issue.description}",
"projectId": "${issue.projectId}"
}"""

}