Skip to content

Commit

Permalink
Merge pull request #8 from isel-leic-daw/feature/dev/gh-2-adds-siren
Browse files Browse the repository at this point in the history
gh-2: adds partial Siren support
  • Loading branch information
pmhsfelix authored Oct 20, 2022
2 parents b1f8f20 + 5d2f4e5 commit 3069500
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,14 @@ package pt.isel.daw.tictactow.http
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import pt.isel.daw.tictactow.http.model.HomeOutputModel
import pt.isel.daw.tictactow.http.model.LinkOutputModel
import pt.isel.daw.tictactow.infra.siren

@RestController
class HomeController {

@GetMapping(Uris.HOME)
fun getHome() = HomeOutputModel(
links = listOf(
LinkOutputModel(
Uris.home(),
LinkRelation.SELF
),
LinkOutputModel(
Uris.home(),
LinkRelation.HOME
),
)
)
fun getHome() = siren(HomeOutputModel("Made for teaching purposes by P. Félix")) {
link(Uris.home(), Rels.SELF)
link(Uris.home(), Rels.HOME)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package pt.isel.daw.tictactow.http

import pt.isel.daw.tictactow.infra.LinkRelation

object Rels {

val SELF = LinkRelation("self")

val HOME = LinkRelation(
"https://github.com/isel-leic-daw/s2223i-51d-51n-public/tree/main/code/tic-tac-tow-service/docs/" +
"rels/home"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import java.net.URI
object Uris {

const val HOME = "/"
const val USER_HOME = "/me"
const val GAME_BY_ID = "/games/{gid}"

const val USERS_CREATE = "/users"
const val USERS_TOKEN = "/users/token"
const val USERS_GET_BY_ID = "/users/{id}"

fun home(): URI = URI(HOME)
fun userHome(): URI = URI(USER_HOME)
fun gameById(game: Game) = UriTemplate(GAME_BY_ID).expand(game.id)

fun userById(id: String) = UriTemplate(USERS_GET_BY_ID).expand(id)
object Users {
const val CREATE = "/users"
const val TOKEN = "/users/token"
const val GET_BY_ID = "/users/{id}"
const val HOME = "/me"

fun byId(id: String) = UriTemplate(GET_BY_ID).expand(id)
fun home(): URI = URI(HOME)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ class UsersController(
private val userService: UsersService
) {

@PostMapping(Uris.USERS_CREATE)
@PostMapping(Uris.Users.CREATE)
fun create(@RequestBody input: UserCreateInputModel): ResponseEntity<*> {
val res = userService.createUser(input.username, input.password)
return when (res) {
is Either.Right -> ResponseEntity.status(201)
.header(
"Location",
Uris.userById(res.value).toASCIIString()
Uris.Users.byId(res.value).toASCIIString()
).build<Unit>()
is Either.Left -> when (res.value) {
UserCreationError.InsecurePassword -> Problem.response(400, Problem.insecurePassword)
Expand All @@ -38,7 +38,7 @@ class UsersController(
}
}

@PostMapping(Uris.USERS_TOKEN)
@PostMapping(Uris.Users.TOKEN)
fun token(@RequestBody input: UserCreateTokenInputModel): ResponseEntity<*> {
val res = userService.createToken(input.username, input.password)
return when (res) {
Expand All @@ -50,12 +50,12 @@ class UsersController(
}
}

@GetMapping(Uris.USERS_GET_BY_ID)
@GetMapping(Uris.Users.GET_BY_ID)
fun getById(@PathVariable id: String) {
TODO("TODO")
}

@GetMapping(Uris.USER_HOME)
@GetMapping(Uris.Users.HOME)
fun getUserHome(user: User): UserHomeOutputModel {
return UserHomeOutputModel(
id = user.id.toString(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package pt.isel.daw.tictactow.http.model

data class HomeOutputModel(
val links: List<LinkOutputModel>,
val credits: String,
)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package pt.isel.daw.tictactow.infra

@JvmInline
value class LinkRelation(
val value: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package pt.isel.daw.tictactow.infra

import com.fasterxml.jackson.annotation.JsonProperty
import org.springframework.http.HttpMethod
import java.net.URI

data class SirenModel<T>(
@get:JsonProperty("class")
val clazz: List<String>,
val properties: T,
val links: List<LinkModel>,
val entities: List<EntityModel<*>>,
val actions: List<ActionModel>
)

data class LinkModel(
val rel: List<String>,
val href: String,
)

data class EntityModel<T>(
val properties: T,
val links: List<LinkModel>,
val rel: List<String>,
)

data class ActionModel(
val name: String,
val href: String,
val method: String,
val type: String,
val fields: List<FieldModel>,
)

data class FieldModel(
val name: String,
val type: String,
val value: String? = null,
)

class SirenBuilderScope<T>(
val properties: T,
) {
private val links = mutableListOf<LinkModel>()
private val entities = mutableListOf<EntityModel<*>>()
private val classes = mutableListOf<String>()
private val actions = mutableListOf<ActionModel>()

fun clazz(value: String) {
classes.add(value)
}

fun link(href: URI, rel: LinkRelation) {
links.add(LinkModel(listOf(rel.value), href.toASCIIString()))
}

fun <U> entity(value: U, rel: LinkRelation, block: EntityBuilderScope<U>.() -> Unit) {
val scope = EntityBuilderScope(value, listOf(rel.value))
scope.block()
entities.add(scope.build())
}

fun action(name: String, href: URI, method: HttpMethod, type: String, block: ActionBuilderScope.() -> Unit) {
val scope = ActionBuilderScope(name, href, method, type)
scope.block()
actions.add(scope.build())
}

fun build(): SirenModel<T> = SirenModel(
clazz = classes,
properties = properties,
links = links,
entities = entities,
actions = actions
)
}

class EntityBuilderScope<T>(
val properties: T,
val rel: List<String>,
) {
private val links = mutableListOf<LinkModel>()

fun link(href: URI, rel: LinkRelation) {
links.add(LinkModel(listOf(rel.value), href.toASCIIString()))
}

fun build(): EntityModel<T> = EntityModel(
properties = properties,
links = links,
rel = rel,
)
}

class ActionBuilderScope(
private val name: String,
private val href: URI,
private val method: HttpMethod,
private val type: String,
) {
private val fields = mutableListOf<FieldModel>()

fun textField(name: String) {
fields.add(FieldModel(name, "text"))
}

fun numberField(name: String) {
fields.add(FieldModel(name, "number"))
}

fun hiddenField(name: String, value: String) {
fields.add(FieldModel(name, "hidden", value))
}

fun build() = ActionModel(name, href.toASCIIString(), method.name, type, fields)
}

fun <T> siren(value: T, block: SirenBuilderScope<T>.() -> Unit): SirenModel<T> {
val scope = SirenBuilderScope(value)
scope.block()
return scope.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package pt.isel.daw.tictactow.infra

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Test
import org.skyscreamer.jsonassert.JSONAssert
import org.springframework.http.HttpMethod
import java.net.URI

class SirenTests {

@Test
fun `can produce siren representation`() {
// given: model classes
class GameModel(
val name: String,
val description: String
)

class PlayerModel(
val name: String,
)

// and: link relations
val self = LinkRelation("self")
val player = LinkRelation("https://example.com/rels/player")

// and: a Jackson mapper
val mapper = ObjectMapper().apply {
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}

// when: producing a Siren model
val sirenModel = siren(
GameModel(
name = "the name",
description = "the description",
)
) {
clazz("game")
link(URI("https://example.com/games/1"), self)
entity(PlayerModel("Alice"), player) {
link(URI("https://example.com/users/1"), self)
}
entity(PlayerModel("Bob"), player) {
link(URI("https://example.com/users/2"), self)
}
action(
"cancel",
URI("https://example.com/games/1/cancel"),
HttpMethod.POST,
"application/json"
) {
textField("reason")
}
}

// and: serializing it to JSON
val jsonString = mapper.writeValueAsString(sirenModel)

// then: the serialization is the expected one
val expected = """
{
"class":["game"],
"properties": {
"name": "the name",
"description": "the description"
},
"entities":[
{
"rel": ["https://example.com/rels/player"],
"properties": {
"name": "Alice"
},
"links": [
{"rel": ["self"], "href": "https://example.com/users/1"}
]
},
{
"rel": ["https://example.com/rels/player"],
"properties": {
"name": "Bob"
},
"links": [
{"rel": ["self"], "href": "https://example.com/users/2"}
]
}
],
"links": [
{"rel": ["self"], "href": "https://example.com/games/1"}
],
"actions": [
{"name": "cancel", "href":"https://example.com/games/1/cancel", "method":"POST",
"type": "application/json",
"fields": [
{"name":"reason", "type": "text"}
]}
]
}
""".trimIndent()
JSONAssert.assertEquals(expected, jsonString, true)
}
}

0 comments on commit 3069500

Please sign in to comment.