Implement first routes & tests
install Koin install kotest declare first events create EventStream
This commit is contained in:
@@ -1,16 +1,17 @@
|
||||
package io.github.flecomte
|
||||
package eventDemo
|
||||
|
||||
import io.github.flecomte.plugins.configureHTTP
|
||||
import io.github.flecomte.plugins.configureRouting
|
||||
import io.github.flecomte.plugins.configureSecurity
|
||||
import io.github.flecomte.plugins.configureSerialization
|
||||
import io.github.flecomte.plugins.configureSockets
|
||||
import eventDemo.plugins.configureHTTP
|
||||
import eventDemo.plugins.configureKoin
|
||||
import eventDemo.plugins.configureRouting
|
||||
import eventDemo.plugins.configureSecurity
|
||||
import eventDemo.plugins.configureSerialization
|
||||
import eventDemo.plugins.configureSockets
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.netty.Netty
|
||||
|
||||
fun main() {
|
||||
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
|
||||
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module, watchPaths = listOf("classes"))
|
||||
.start(wait = true)
|
||||
}
|
||||
|
||||
@@ -20,4 +21,5 @@ fun Application.module() {
|
||||
configureSockets()
|
||||
configureHTTP()
|
||||
configureRouting()
|
||||
configureKoin()
|
||||
}
|
||||
14
src/main/kotlin/eventDemo/app/AggregateId.kt
Normal file
14
src/main/kotlin/eventDemo/app/AggregateId.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package eventDemo.app
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
sealed interface AggregateId {
|
||||
val id: UUID
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class GameId(override val id: UUID = UUID.randomUUID()) : AggregateId {
|
||||
constructor(id: String) : this(UUID.fromString(id))
|
||||
|
||||
override fun toString(): String = id.toString()
|
||||
}
|
||||
30
src/main/kotlin/eventDemo/app/Card.kt
Normal file
30
src/main/kotlin/eventDemo/app/Card.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
package eventDemo.app
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface Card {
|
||||
@Serializable
|
||||
enum class Color {
|
||||
Blue,
|
||||
Red,
|
||||
Yellow,
|
||||
Green,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("Simple")
|
||||
data class Simple(
|
||||
val number: Int,
|
||||
val color: Color,
|
||||
) : Card
|
||||
|
||||
sealed interface Special : Card
|
||||
|
||||
@Serializable
|
||||
@SerialName("Reverse")
|
||||
data class ReverseCard(
|
||||
val color: Color,
|
||||
) : Special
|
||||
}
|
||||
27
src/main/kotlin/eventDemo/app/EventStream.kt
Normal file
27
src/main/kotlin/eventDemo/app/EventStream.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package eventDemo.app
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
|
||||
class EventStream<ID : AggregateId> {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val eventBus: MutableMap<ID, MutableList<Event<ID>>> = mutableMapOf()
|
||||
|
||||
fun publish(event: Event<ID>) {
|
||||
eventBus.getOrPut(event.aggregateId) { mutableListOf() }.add(event)
|
||||
logger.atInfo {
|
||||
message = "Event published"
|
||||
payload = mapOf("event" to event)
|
||||
}
|
||||
}
|
||||
|
||||
fun <U : Event<ID>> read(
|
||||
aggregateId: ID,
|
||||
eventClass: Class<U>,
|
||||
): U? {
|
||||
return eventBus.get(aggregateId)?.filterIsInstance(eventClass)?.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified U : Event<ID>, ID : AggregateId> EventStream<ID>.read(aggregateId: ID): U? {
|
||||
return this.read(aggregateId, U::class.java)
|
||||
}
|
||||
10
src/main/kotlin/eventDemo/app/Events.kt
Normal file
10
src/main/kotlin/eventDemo/app/Events.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package eventDemo.app
|
||||
|
||||
sealed interface Event<ID : AggregateId> {
|
||||
val aggregateId: ID
|
||||
}
|
||||
|
||||
data class PlayCardEvent(
|
||||
override val aggregateId: GameId,
|
||||
val card: Card,
|
||||
) : Event<GameId>
|
||||
54
src/main/kotlin/eventDemo/app/actions/PutCard.kt
Normal file
54
src/main/kotlin/eventDemo/app/actions/PutCard.kt
Normal file
@@ -0,0 +1,54 @@
|
||||
package eventDemo.app.actions
|
||||
|
||||
import eventDemo.app.Card
|
||||
import eventDemo.app.EventStream
|
||||
import eventDemo.app.GameId
|
||||
import eventDemo.app.PlayCardEvent
|
||||
import eventDemo.app.read
|
||||
import eventDemo.plugins.GameIdSerializer
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.resources.Resource
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.request.receive
|
||||
import io.ktor.server.resources.get
|
||||
import io.ktor.server.resources.post
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondNullable
|
||||
import io.ktor.server.routing.Routing
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.ktor.ext.inject
|
||||
|
||||
@Serializable
|
||||
@Resource("/game/{id}")
|
||||
class Game(
|
||||
@Serializable(with = GameIdSerializer::class)
|
||||
val id: GameId,
|
||||
) {
|
||||
@Serializable
|
||||
@Resource("card")
|
||||
class Card(val game: Game) {
|
||||
@Serializable
|
||||
@Resource("")
|
||||
class PutCard(val card: Card)
|
||||
|
||||
@Serializable
|
||||
@Resource("last")
|
||||
class LastCard(val card: Card)
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.putCard() {
|
||||
val eventStream by inject<EventStream<GameId>>()
|
||||
|
||||
post<Game.Card.PutCard> {
|
||||
val card = call.receive<Card.Simple>()
|
||||
eventStream.publish(PlayCardEvent(it.card.game.id, card))
|
||||
call.respondNullable<Any?>(HttpStatusCode.OK, null)
|
||||
}
|
||||
|
||||
get<Game.Card.LastCard> {
|
||||
eventStream.read<PlayCardEvent, GameId>(it.card.game.id)
|
||||
?.let { it1 -> call.respond<Card>(it1.card) }
|
||||
?: call.response.status(HttpStatusCode.BadRequest)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package io.github.flecomte.plugins
|
||||
package eventDemo.plugins
|
||||
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.plugins.cors.routing.CORS
|
||||
@@ -10,6 +11,7 @@ fun Application.configureHTTP() {
|
||||
install(CORS) {
|
||||
allowMethod(HttpMethod.Options)
|
||||
allowMethod(HttpMethod.Put)
|
||||
allowMethod(HttpMethod.Post)
|
||||
allowMethod(HttpMethod.Delete)
|
||||
allowMethod(HttpMethod.Patch)
|
||||
allowHeader(HttpHeaders.Authorization)
|
||||
@@ -17,3 +19,18 @@ fun Application.configureHTTP() {
|
||||
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
|
||||
}
|
||||
}
|
||||
|
||||
class BadRequestException(val httpError: HttpErrorBadRequest) : Exception()
|
||||
|
||||
class HttpErrorBadRequest(
|
||||
statusCode: HttpStatusCode,
|
||||
val title: String = statusCode.description,
|
||||
val invalidParams: List<InvalidParam>,
|
||||
) {
|
||||
val statusCode: Int = statusCode.value
|
||||
|
||||
data class InvalidParam(
|
||||
val name: String,
|
||||
val reason: String,
|
||||
)
|
||||
}
|
||||
22
src/main/kotlin/eventDemo/plugins/Koin.kt
Normal file
22
src/main/kotlin/eventDemo/plugins/Koin.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package eventDemo.plugins
|
||||
|
||||
import eventDemo.app.EventStream
|
||||
import eventDemo.app.GameId
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
import org.koin.ktor.plugin.Koin
|
||||
import org.koin.logger.slf4jLogger
|
||||
|
||||
fun Application.configureKoin() {
|
||||
install(Koin) {
|
||||
slf4jLogger()
|
||||
modules(appModule)
|
||||
}
|
||||
}
|
||||
|
||||
val appModule =
|
||||
module {
|
||||
singleOf<EventStream<GameId>>(::EventStream)
|
||||
}
|
||||
@@ -1,39 +1,31 @@
|
||||
package io.github.flecomte.plugins
|
||||
package eventDemo.plugins
|
||||
|
||||
import eventDemo.app.actions.putCard
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.resources.Resource
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.plugins.autohead.AutoHeadResponse
|
||||
import io.ktor.server.plugins.statuspages.StatusPages
|
||||
import io.ktor.server.resources.Resources
|
||||
import io.ktor.server.resources.get
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondText
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.routing
|
||||
import kotlinx.serialization.Serializable
|
||||
import io.ktor.util.converters.DataConversion.Configuration
|
||||
|
||||
fun Application.configureRouting() {
|
||||
install(AutoHeadResponse)
|
||||
install(Resources)
|
||||
install(StatusPages) {
|
||||
exception<BadRequestException> { call, cause ->
|
||||
call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest)
|
||||
}
|
||||
exception<Throwable> { call, cause ->
|
||||
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
routing {
|
||||
get("/") {
|
||||
call.respondText("Hello World!")
|
||||
}
|
||||
get<Articles> { article ->
|
||||
// Get all articles ...
|
||||
call.respond("List of articles sorted starting from ${article.sort}")
|
||||
}
|
||||
putCard()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@Resource("/articles")
|
||||
class Articles(val sort: String? = "new")
|
||||
private typealias ConverterDeclaration = Configuration.() -> Unit
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.github.flecomte.plugins
|
||||
package eventDemo.plugins
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
55
src/main/kotlin/eventDemo/plugins/Serialization.kt
Normal file
55
src/main/kotlin/eventDemo/plugins/Serialization.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package eventDemo.plugins
|
||||
|
||||
import eventDemo.app.GameId
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import java.util.UUID
|
||||
|
||||
fun Application.configureSerialization() {
|
||||
install(ContentNegotiation) {
|
||||
json(
|
||||
Json {
|
||||
serializersModule =
|
||||
SerializersModule {
|
||||
contextual(UUID::class) { UUIDSerializer }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object GameIdSerializer : KSerializer<GameId> {
|
||||
override fun deserialize(decoder: Decoder): GameId = GameId(UUID.fromString(decoder.decodeString()))
|
||||
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: GameId,
|
||||
) {
|
||||
encoder.encodeString(value.id.toString())
|
||||
}
|
||||
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("GameId", PrimitiveKind.STRING)
|
||||
}
|
||||
|
||||
object UUIDSerializer : KSerializer<UUID> {
|
||||
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
|
||||
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: UUID,
|
||||
) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.github.flecomte.plugins
|
||||
package eventDemo.plugins
|
||||
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
@@ -1,21 +0,0 @@
|
||||
package io.github.flecomte.plugins
|
||||
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.routing
|
||||
|
||||
fun Application.configureSerialization() {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
routing {
|
||||
get("/json/kotlinx-serialization") {
|
||||
call.respond(mapOf("hello" to "world"))
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/test/kotlin/eventDemo/app/actions/Client.kt
Normal file
25
src/test/kotlin/eventDemo/app/actions/Client.kt
Normal file
@@ -0,0 +1,25 @@
|
||||
package eventDemo.app.actions
|
||||
|
||||
import eventDemo.plugins.UUIDSerializer
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.testing.ApplicationTestBuilder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import java.util.UUID
|
||||
|
||||
fun ApplicationTestBuilder.httpClient(): HttpClient {
|
||||
return createClient {
|
||||
install(ContentNegotiation) {
|
||||
json(
|
||||
Json {
|
||||
serializersModule =
|
||||
SerializersModule {
|
||||
contextual(UUID::class) { UUIDSerializer }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/test/kotlin/eventDemo/app/actions/PutCardTest.kt
Normal file
61
src/test/kotlin/eventDemo/app/actions/PutCardTest.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
package eventDemo.app.actions
|
||||
|
||||
import eventDemo.app.Card
|
||||
import eventDemo.app.EventStream
|
||||
import eventDemo.app.GameId
|
||||
import eventDemo.app.PlayCardEvent
|
||||
import eventDemo.module
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.accept
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType.Application.Json
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.server.testing.testApplication
|
||||
import org.koin.core.context.stopKoin
|
||||
import org.koin.ktor.ext.inject
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class PutCardTest : FunSpec({
|
||||
test("/game/{id}/card") {
|
||||
testApplication {
|
||||
val client = httpClient()
|
||||
application {
|
||||
stopKoin()
|
||||
module()
|
||||
}
|
||||
val id = GameId().toString()
|
||||
client.post("/game/$id/card") {
|
||||
contentType(Json)
|
||||
accept(Json)
|
||||
setBody(Card.Simple(1, Card.Color.Blue))
|
||||
}.apply {
|
||||
assertEquals(status, HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("/game/{id}/card/last") {
|
||||
testApplication {
|
||||
val client = httpClient()
|
||||
val id = GameId()
|
||||
val card = Card.Simple(1, Card.Color.Blue)
|
||||
application {
|
||||
stopKoin()
|
||||
module()
|
||||
val eventStream by inject<EventStream<GameId>>()
|
||||
eventStream.publish(
|
||||
PlayCardEvent(id, card),
|
||||
)
|
||||
}
|
||||
|
||||
client.get("/game/$id/card/last").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(card, this.call.body<Card>())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
package io.github.flecomte
|
||||
|
||||
import io.github.flecomte.plugins.configureRouting
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.testing.testApplication
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ApplicationTest {
|
||||
@Test
|
||||
fun testRoot() =
|
||||
testApplication {
|
||||
application {
|
||||
configureRouting()
|
||||
}
|
||||
client.get("/").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals("Hello World!", bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user