Skip to content

Commit

Permalink
Story #4: introduce language parameter to the API
Browse files Browse the repository at this point in the history
  • Loading branch information
ayashkov committed Feb 24, 2024
1 parent be005d9 commit 68a091e
Show file tree
Hide file tree
Showing 18 changed files with 102 additions and 67 deletions.
2 changes: 1 addition & 1 deletion app/src/test/doc/io/granito/segovia/spec/api/Api.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ will contain JSON with the following HAL links:
<pre concordion:assert-equals="containsJson(#response.body, #TEXT)">{
"_links": {
"self": { "href": "/api/v1" },
"sentences": { "href": "/api/v1/sentences" }
"sentences": { "href": "/api/v1/languages/es/sentences" }
}
}</pre>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

All examples below assume that the application has only
"[Roberto se había levantado de la cama.](- "#sentence")"
[sentence](- "store(#sentence)").
[sentence](- "store('es', #sentence)").

### ~~Common assumptions~~

Expand All @@ -15,23 +15,23 @@ collection.
### [Get sentences](-)

When a client makes a
**[GET](- "#method") [/api/v1/sentences](- "#uri")**
**[GET](- "#method") [/api/v1/languages/es/sentences](- "#uri")**
[HTTP request](- "#response=http(#method, #uri)"), then the application
responds with [200](- "?=#response.status") HTTP status and
[application/hal+json](- "?=#response.contentType") body containing
JSON with at least following properties:

<pre concordion:assert-equals="containsJson(#response.body, #TEXT)">{
"_links": {
"self": { "href": "/api/v1/sentences" }
"self": { "href": "/api/v1/languages/es/sentences" }
},
"_embedded": {
"sentences": [
{
"id": "h6kLGAVxboVG",
"text": "Roberto se había levantado de la cama.",
"_links": {
"self": { "href": "/api/v1/sentences/h6kLGAVxboVG" }
"self": { "href": "/api/v1/languages/es/sentences/h6kLGAVxboVG" }
}
}
]
Expand All @@ -48,7 +48,8 @@ it will be returned as a HAL resource.
### [Get sentence](-)

When a client makes a
**[GET](- "#method") [/api/v1/sentences/h6kLGAVxboVG](- "#uri")**
**[GET](- "#method")
[/api/v1/languages/es/sentences/h6kLGAVxboVG](- "#uri")**
[HTTP request](- "#response=http(#method, #uri)"), then the application
responds with [200](- "?=#response.status") HTTP status and
[application/hal+json](- "?=#response.contentType") body containing
Expand All @@ -58,7 +59,7 @@ JSON with at least following properties:
"id": "h6kLGAVxboVG",
"text": "Roberto se había levantado de la cama.",
"_links": {
"self": { "href": "/api/v1/sentences/h6kLGAVxboVG" }
"self": { "href": "/api/v1/languages/es/sentences/h6kLGAVxboVG" }
}
}</pre>

Expand All @@ -70,7 +71,7 @@ respond with `404` HTTP status code.
### [Not found](-)

When a client makes a
**[GET](- "#method") [/api/v1/sentences/unknown](- "#uri")**
**[GET](- "#method") [/api/v1/languages/es/sentences/unknown](- "#uri")**
[HTTP request](- "#response=http(#method, #uri)"), then the application
responds with [404](- "?=#response.status") HTTP status and
[application/problem+json](- "?=#response.contentType") body containing
Expand All @@ -81,7 +82,7 @@ JSON with at least following properties:
"type": "https://segovia.granito.io/problem/not-found/sentence",
"title": "Sentence is not found.",
"detail": "Sentence identified by 'unknown' is not found.",
"instance": "/api/v1/sentences/unknown"
"instance": "/api/v1/languages/es/sentences/unknown"
}</pre>

### ~~Not found~~
2 changes: 1 addition & 1 deletion app/src/test/doc/io/granito/segovia/spec/ui/Ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ language using a text in the target language.

Given that the application has
"[Roberto se había levantado de la cama.](- "#sentence")"
[sentence](- "store(#sentence)"), when a user accesses
[sentence](- "store('es', #sentence)"), when a user accesses
**[/study](- "load(#TEXT)")** URI, then:

* the browser navigates to [/study](- "?=uri") URI;
Expand Down
5 changes: 3 additions & 2 deletions app/src/test/kotlin/io/granito/segovia/spec/SpecBase.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.granito.segovia.spec

import io.granito.segovia.core.model.langFrom
import io.granito.segovia.core.repo.SentenceRepo
import io.granito.segovia.core.usecase.CreateSentenceCase
import org.concordion.api.AfterExample
Expand Down Expand Up @@ -28,8 +29,8 @@ abstract class SpecBase {
sentenceRepo.clear().block()
}

fun store(text: String) {
createSentenceCase.create(text).block()
fun store(lang: String, text: String) {
createSentenceCase.create(langFrom(lang), text).block()
}

fun contains(string: String?, sub: String?) =
Expand Down
12 changes: 12 additions & 0 deletions core/src/main/kotlin/io/granito/segovia/core/model/Lang.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.granito.segovia.core.model

fun langFrom(code: String) = Lang.valueOf(code.uppercase())

enum class Lang {
EN,
ES,
FR,
IT;

override fun toString() = name.lowercase()
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package io.granito.segovia.core.model

class Sentence(val id: Slug, val text: String) {
constructor(text: String): this(Slug.of(text), text)
class Sentence(val lang: Lang, val id: Slug, val text: String) {
constructor(lang: Lang, text: String): this(lang, Slug.of(text), text)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.granito.segovia.core.service

import io.granito.segovia.core.model.Lang
import io.granito.segovia.core.model.Sentence
import io.granito.segovia.core.model.Slug
import io.granito.segovia.core.repo.SentenceRepo
Expand All @@ -11,23 +12,23 @@ import reactor.core.publisher.Mono

class SentenceService(private val sentenceRepo: SentenceRepo):
SearchSentencesCase, FetchSentenceCase, CreateSentenceCase {
override fun search(): Flux<Sentence> =
override fun search(lang: Lang): Flux<Sentence> =
try {
sentenceRepo.select()
} catch (ex: Exception) {
Flux.error(ex)
}

override fun fetch(id: Slug): Mono<Sentence> =
override fun fetch(lang: Lang, id: Slug): Mono<Sentence> =
try {
sentenceRepo.load(id)
} catch (ex: Exception) {
Mono.error(ex)
}

override fun create(text: String): Mono<Sentence> =
override fun create(lang: Lang, text: String): Mono<Sentence> =
try {
val sentence = Sentence(text)
val sentence = Sentence(lang, text)

sentenceRepo.insert(sentence).thenReturn(sentence)
} catch (ex: Exception) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package io.granito.segovia.core.usecase

import io.granito.segovia.core.model.Lang
import io.granito.segovia.core.model.Sentence
import reactor.core.publisher.Mono

interface CreateSentenceCase {
fun create(text: String): Mono<Sentence>
fun create(lang: Lang, text: String): Mono<Sentence>
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package io.granito.segovia.core.usecase

import io.granito.segovia.core.model.Lang
import io.granito.segovia.core.model.Sentence
import io.granito.segovia.core.model.Slug
import reactor.core.publisher.Mono

interface FetchSentenceCase {
fun fetch(id: Slug): Mono<Sentence>
fun fetch(lang: Lang, id: Slug): Mono<Sentence>
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package io.granito.segovia.core.usecase

import io.granito.segovia.core.model.Lang
import io.granito.segovia.core.model.Sentence
import reactor.core.publisher.Flux

interface SearchSentencesCase {
fun search(): Flux<Sentence>
fun search(lang: Lang): Flux<Sentence>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.granito.segovia.core.usecase

import io.granito.segovia.core.model.Lang
import io.granito.segovia.core.model.Slug
import io.granito.segovia.core.repo.SentenceRepo
import io.granito.segovia.core.service.SentenceService
Expand Down Expand Up @@ -37,7 +38,7 @@ class CreateSentenceCaseTest {
id == slug && text == sentence
})

StepVerifier.create(service.create("Llueve mucho."))
StepVerifier.create(service.create(Lang.ES, "Llueve mucho."))
.assertNext {
assertThat(it.id).isEqualTo(slug)
assertThat(it.text).isEqualTo(sentence)
Expand All @@ -54,7 +55,7 @@ class CreateSentenceCaseTest {

doReturn(result).whenever(sentenceRepo).insert(any())

StepVerifier.create(service.create("Se acabo."))
StepVerifier.create(service.create(Lang.ES, "Se acabo."))
.verifyErrorSatisfies {
assertThat(it).isSameAs(t)
}
Expand All @@ -66,7 +67,7 @@ class CreateSentenceCaseTest {

doThrow(t).whenever(sentenceRepo).insert(any())

StepVerifier.create(service.create("Se acabo."))
StepVerifier.create(service.create(Lang.ES, "Se acabo."))
.verifyErrorSatisfies {
assertThat(it).isSameAs(t)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.granito.segovia.core.usecase

import io.granito.segovia.core.model.Lang
import io.granito.segovia.core.model.Sentence
import io.granito.segovia.core.model.Slug
import io.granito.segovia.core.repo.SentenceRepo
Expand Down Expand Up @@ -35,7 +36,7 @@ class FetchSentenceCaseTest {
fun `fetch() returns same data as repo when load succeeds`() {
doReturn(result).whenever(sentenceRepo).load(id)

assertThat(service.fetch(id)).isSameAs(result)
assertThat(service.fetch(Lang.EN, id)).isSameAs(result)
}

@Test
Expand All @@ -44,7 +45,7 @@ class FetchSentenceCaseTest {

doThrow(t).whenever(sentenceRepo).load(any())

StepVerifier.create(service.fetch(id))
StepVerifier.create(service.fetch(Lang.EN, id))
.verifyErrorSatisfies {
assertThat(it).isSameAs(t)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.granito.segovia.core.usecase

import io.granito.segovia.core.model.Lang
import io.granito.segovia.core.model.Sentence
import io.granito.segovia.core.repo.SentenceRepo
import io.granito.segovia.core.service.SentenceService
Expand Down Expand Up @@ -31,7 +32,7 @@ class SearchSentencesCaseTest {
fun `search() returns same data as repo when select succeeds`() {
doReturn(result).whenever(sentenceRepo).select()

assertThat(service.search()).isSameAs(result)
assertThat(service.search(Lang.EN)).isSameAs(result)
}

@Test
Expand All @@ -40,7 +41,7 @@ class SearchSentencesCaseTest {

doThrow(t).whenever(sentenceRepo).select()

StepVerifier.create(service.search())
StepVerifier.create(service.search(Lang.ES))
.verifyErrorSatisfies {
assertThat(it).isSameAs(t)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.granito.segovia.web.controller

import io.granito.segovia.core.model.Slug
import io.granito.segovia.core.model.langFrom
import io.granito.segovia.core.usecase.FetchSentenceCase
import io.granito.segovia.core.usecase.SearchSentencesCase
import io.granito.segovia.web.model.SentenceNotFoundException
Expand All @@ -14,7 +15,9 @@ import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Mono

const val SENTENCES = "$ROOT/sentences"
const val LANGUAGES = "$ROOT/languages"
const val LANGUAGE = "$LANGUAGES/{lang}"
const val SENTENCES = "$LANGUAGE/sentences"
const val SENTENCE = "$SENTENCES/{id}"

@RestController
Expand All @@ -23,24 +26,28 @@ class SentenceController(
private val searchSentencesCase: SearchSentencesCase,
private val fetchSentenceCase: FetchSentenceCase) {
@GetMapping(SENTENCES)
fun get(): Mono<CollectionModel<SentenceResource>> =
fun get(@PathVariable("lang") code: String):
Mono<CollectionModel<SentenceResource>> =
try {
searchSentencesCase.search()
val lang = langFrom(code)

searchSentencesCase.search(lang)
.map { SentenceResource(it) }
.collectList()
.map {
CollectionModel.of(it)
.withFallbackType(SentenceResource::class.java)
.add(Link.of(SENTENCES))
.add(Link.of(SENTENCES).expand(lang))
}
} catch (ex: Exception) {
Mono.error(ex)
}

@GetMapping(SENTENCE)
fun getOne(@PathVariable("id") id: String): Mono<SentenceResource> =
fun getOne(@PathVariable("lang") code: String,
@PathVariable("id") id: String): Mono<SentenceResource> =
try {
fetchSentenceCase.fetch(Slug(id))
fetchSentenceCase.fetch(langFrom(code), Slug(id))
.map { SentenceResource(it) }
.switchIfEmpty(Mono.error(SentenceNotFoundException(id)))
} catch (ex: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.granito.segovia.web.controller.ROOT
import io.granito.segovia.web.controller.SENTENCES
import org.springframework.hateoas.Link
import org.springframework.hateoas.RepresentationModel
import org.springframework.hateoas.UriTemplate
import org.springframework.hateoas.server.core.Relation

@Relation(itemRelation = "root")
Expand All @@ -16,7 +17,7 @@ class RootResource(val status: String, val apiVersion: String):
init {
add(
Link.of(ROOT),
Link.of(SENTENCES, "sentences")
Link.of(UriTemplate.of(SENTENCES), "sentences").expand("es")
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import org.springframework.hateoas.RepresentationModel
import org.springframework.hateoas.server.core.Relation

@Relation(itemRelation = "sentence", collectionRelation = "sentences")
class SentenceResource(val id: String, val text: String):
class SentenceResource(language: String, val id: String, val text: String):
RepresentationModel<SentenceResource>() {
constructor(sentence: Sentence):
this(sentence.id.toString(), sentence.text)
this(sentence.lang.toString(), sentence.id.toString(), sentence.text)

init {
add(Link.of(SENTENCE).expand(id))
add(Link.of(SENTENCE).expand(language, id))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ internal class RootControllerTest {
StepVerifier.create(controller.get())
.assertNext {
assertThat(it.getRequiredLink("sentences").href)
.isEqualTo("/api/v1/sentences")
.isEqualTo("/api/v1/languages/es/sentences")
}
.verifyComplete()
}
Expand Down
Loading

0 comments on commit 68a091e

Please sign in to comment.