This commit is contained in:
2024-05-30 21:41:02 +02:00
parent 03ba14d918
commit ae5c229e4b
32 changed files with 537 additions and 344 deletions

View File

@@ -3,15 +3,16 @@
@Suppress("ktlint:standard:property-naming") @Suppress("ktlint:standard:property-naming")
val ktor_version: String by project val ktor_version: String by project
val kotlin_version: String by project val kotlin_version: String by project
val kotlin_serialization_version: String by project
val logback_version: String by project val logback_version: String by project
val koin_version: String by project val koin_version: String by project
val kotlin_logging_version: String by project val kotlin_logging_version: String by project
val kotest_version: String by project val kotest_version: String by project
plugins { plugins {
kotlin("jvm") version "1.9.22" kotlin("jvm") version "2.1.10"
id("io.ktor.plugin") version "2.3.8" id("io.ktor.plugin") version "2.3.8"
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22" id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10"
id("org.jlleitschuh.gradle.ktlint") version "12.1.0" id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
} }
@@ -52,8 +53,9 @@ dependencies {
implementation("io.insert-koin:koin-ktor:$koin_version") implementation("io.insert-koin:koin-ktor:$koin_version")
implementation("io.insert-koin:koin-logger-slf4j:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
implementation("io.github.oshai:kotlin-logging-jvm:$kotlin_logging_version") implementation("io.github.oshai:kotlin-logging-jvm:$kotlin_logging_version")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlin_serialization_version")
testImplementation("io.ktor:ktor-server-tests-jvm") testImplementation("io.ktor:ktor-server-tests-jvm")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
testImplementation("io.ktor:ktor-server-test-host-jvm:2.3.8") testImplementation("io.ktor:ktor-server-test-host-jvm:2.3.11")
testImplementation("io.kotest:kotest-runner-junit5:$kotest_version") testImplementation("io.kotest:kotest-runner-junit5:$kotest_version")
} }

View File

@@ -1,8 +1,13 @@
ktor_version=2.3.8 ktor_version=2.3.13
kotlin_version=1.9.22 #ktor_version=3.0.3
logback_version=1.4.14 kotlin_version=2.1.10
koin_version=3.5.3 kotlin_serialization_version=1.8.0
logback_version=1.5.16
koin_version=3.5.6
# koin_version=4.0.2
kotlin_logging_version=5.1.0 kotlin_logging_version=5.1.0
kotest_version=5.8.0 #kotlin_logging_version=7.0.4
kotest_version=5.9.1
#kotest_version=6.0.0
kotlin.code.style=official kotlin.code.style=official

View File

@@ -5,4 +5,58 @@ info:
version: "1.0.0" version: "1.0.0"
servers: servers:
- url: "https://event_demo" - url: "https://event_demo"
paths: {} paths:
"/game/{id}/card/last":
get:
description: get the last card played
responses:
200:
description: The last card
content:
application/json:
schema:
$ref: "#/components/schemas/Card"
components:
schemas:
Card:
oneOf:
- $ref: "#/components/schemas/SimpleCard"
- $ref: "#/components/schemas/ReverseCard"
- $ref: "#/components/schemas/PassCard"
- $ref: "#/components/schemas/Plus2Card"
- $ref: "#/components/schemas/Plus4Card"
- $ref: "#/components/schemas/ChangeColorCard"
SimpleCard:
properties:
number:
type: integer
color:
$ref: "#/components/schemas/CardColor"
ReverseCard:
properties:
color:
$ref: "#/components/schemas/CardColor"
PassCard:
properties:
color:
$ref: "#/components/schemas/CardColor"
Plus2Card:
properties:
color:
$ref: "#/components/schemas/CardColor"
Plus4Card:
properties:
nextColor:
$ref: "#/components/schemas/CardColor"
ChangeColorCard:
properties:
nextColor:
$ref: "#/components/schemas/CardColor"
CardColor:
type: string
enum:
- Blue
- Red
- Yellow
- Green

View File

@@ -1,5 +1,6 @@
package eventDemo package eventDemo
import eventDemo.plugins.configureCommandHandler
import eventDemo.plugins.configureHTTP import eventDemo.plugins.configureHTTP
import eventDemo.plugins.configureKoin import eventDemo.plugins.configureKoin
import eventDemo.plugins.configureRouting import eventDemo.plugins.configureRouting
@@ -22,4 +23,5 @@ fun Application.module() {
configureHTTP() configureHTTP()
configureRouting() configureRouting()
configureKoin() configureKoin()
configureCommandHandler()
} }

View File

@@ -1,39 +0,0 @@
package eventDemo.app
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Game(
val id: GameId,
) {
companion object {
fun new(): Game = Game(GameId())
}
}
@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
}

View File

@@ -1,24 +0,0 @@
package eventDemo.app
import io.github.oshai.kotlinlogging.KotlinLogging
class CommandStream {
private val logger = KotlinLogging.logger {}
private val commandBus: MutableList<Command> = mutableListOf()
fun sendRequest(command: Command) {
commandBus.add(command)
logger.atInfo {
message = "Command published: $command"
payload = mapOf("command" to command)
}
}
fun sendRequest(vararg commands: Command) {
commands.forEach { sendRequest(it) }
}
fun readNext(): Command? = commandBus.firstOrNull()
fun <U : Command> readNext(commandClass: Class<U>): U? = commandBus.filterIsInstance(commandClass).firstOrNull()
}

View File

@@ -1,27 +0,0 @@
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.id) { mutableListOf() }.add(event)
logger.atInfo {
message = "Event published: $event"
payload = mapOf("event" to event)
}
}
fun publish(vararg events: Event<ID>) {
events.forEach { publish(it) }
}
fun <U : Event<ID>> read(
aggregateId: ID,
eventClass: Class<U>,
): U? = eventBus.get(aggregateId)?.filterIsInstance(eventClass)?.firstOrNull()
}
inline fun <reified U : Event<ID>, ID : AggregateId> EventStream<ID>.read(aggregateId: ID): U? = this.read(aggregateId, U::class.java)

View File

@@ -1,10 +0,0 @@
package eventDemo.app
sealed interface Event<ID : AggregateId> {
val id: ID
}
data class PlayCardEvent(
override val id: GameId,
val card: Card,
) : Event<GameId>

View File

@@ -1,61 +0,0 @@
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.card() {
val eventStream by inject<EventStream<GameId>>()
post<Game.Card.PutCard> {
val card = call.receive<Card>()
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)
}
}

View File

@@ -1,56 +0,0 @@
package eventDemo.app.actions
import eventDemo.app.Command
import eventDemo.app.CommandId
import eventDemo.app.CommandStream
import eventDemo.app.PlayCardCommand
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.routing.Routing
import kotlinx.serialization.Serializable
import org.koin.ktor.ext.inject
@Serializable
@Resource("/command")
class CommandRoute {
@Resource("send")
class Send(
val commandRoute: CommandRoute,
) {
@Serializable
data class Response(
val id: CommandId,
) {
constructor(command: Command) : this(command.id)
}
}
@Resource("next")
class Next(
val commandRoute: CommandRoute,
)
}
fun Routing.command() {
val commandStream by inject<CommandStream>()
post<CommandRoute.Send> {
val command = call.receive<PlayCardCommand>()
commandStream.sendRequest(command)
call.respond(HttpStatusCode.OK, CommandRoute.Send.Response(command))
}
get<CommandRoute.Next> {
val command = commandStream.readNext()
if (command == null) {
call.response.status(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.OK, command)
}
}
}

View File

@@ -0,0 +1,50 @@
package eventDemo.app.actions.playNewCard
import eventDemo.libs.command.send
import eventDemo.plugins.GameIdSerializer
import eventDemo.shared.GameId
import eventDemo.shared.command.GameCommandStream
import eventDemo.shared.entity.Card
import eventDemo.shared.entity.Game
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.post
import io.ktor.server.response.respondNullable
import io.ktor.server.routing.Routing
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import org.koin.ktor.ext.inject
@Serializable
@Resource("/game/{id}")
class GameRoute(
@Serializable(with = GameIdSerializer::class)
val id: GameId,
) {
@Serializable
@Resource("card")
class Card(
val game: GameRoute,
)
}
fun Routing.playNewCard() {
val commandStream by inject<GameCommandStream>()
/*
* A player request to play a new card.
*
* It always returns [HttpStatusCode.OK], but it is not mean that card is already played!
*/
post<GameRoute.Card> {
val card = call.receive<Card>()
launch(Dispatchers.Default) {
commandStream.send(PlayCardCommand(Game(it.game.id), card))
}
call.respondNullable<Any?>(HttpStatusCode.OK, null)
}
}

View File

@@ -1,25 +1,11 @@
package eventDemo.app package eventDemo.app.actions.playNewCard
import eventDemo.plugins.CommandIdSerializer import eventDemo.libs.command.Command
import eventDemo.libs.command.CommandId
import eventDemo.shared.entity.Card
import eventDemo.shared.entity.Game
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID
@JvmInline
@Serializable(with = CommandIdSerializer::class)
value class CommandId(
private val id: UUID = UUID.randomUUID(),
) {
constructor(id: String) : this(UUID.fromString(id))
override fun toString(): String = id.toString()
}
@Serializable
sealed interface Command {
val id: CommandId
val name: String
}
@Serializable @Serializable
@SerialName("PlayCard") @SerialName("PlayCard")

View File

@@ -0,0 +1,26 @@
package eventDemo.app.actions.playNewCard
import eventDemo.shared.command.GameCommandStream
import eventDemo.shared.event.CardIsPlayedEvent
import eventDemo.shared.event.GameEventStream
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* Listen [PlayCardCommand] on [GameCommandStream], check the validity and execute the action.
* This action produces a new [CardIsPlayedEvent]
*/
class PlayCardCommandHandler(
private val commandStream: GameCommandStream,
private val eventStream: GameEventStream,
) {
operator fun invoke() {
CoroutineScope(Dispatchers.IO).launch {
commandStream.process {
// TODO check the command can be executed
eventStream.publish(CardIsPlayedEvent(it.payload.game.id, it.payload.card))
}
}
}
}

View File

@@ -0,0 +1,42 @@
package eventDemo.app.actions.readLastPlayedCard
import eventDemo.libs.event.readLastOf
import eventDemo.plugins.GameIdSerializer
import eventDemo.shared.GameId
import eventDemo.shared.event.CardIsPlayedEvent
import eventDemo.shared.event.GameEventStream
import io.ktor.http.HttpStatusCode
import io.ktor.resources.Resource
import io.ktor.server.application.call
import io.ktor.server.resources.get
import io.ktor.server.response.respond
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/last")
class Card(
val game: Game,
)
}
fun Routing.readLastPlayedCard() {
val eventStream by inject<GameEventStream>()
/*
* Read the last played card on the game.
*/
get<Game.Card> { card ->
eventStream
.readLastOf<CardIsPlayedEvent, _, _>(card.game.id)
?.let { call.respond(it.card) }
?: call.response.status(HttpStatusCode.BadRequest)
}
}

View File

@@ -0,0 +1,20 @@
package eventDemo.libs.command
import eventDemo.plugins.CommandIdSerializer
import kotlinx.serialization.Serializable
import java.util.UUID
@JvmInline
@Serializable(with = CommandIdSerializer::class)
value class CommandId(
private val id: UUID = UUID.randomUUID(),
) {
constructor(id: String) : this(UUID.fromString(id))
override fun toString(): String = id.toString()
}
interface Command {
val id: CommandId
val name: String
}

View File

@@ -0,0 +1,36 @@
package eventDemo.libs.command
import kotlin.reflect.KClass
interface CommandStream<C : Command> {
/**
* Send a new [Command] to the queue.
*/
suspend fun send(
type: KClass<C>,
command: C,
)
/**
* Send multiple [Command] to the queue.
*/
suspend fun send(
type: KClass<C>,
vararg commands: C,
) {
commands.forEach { send(type, it) }
}
/**
* A class to implement succes/faild action.
*/
interface ComputeStatus {
fun ack()
fun nack()
}
suspend fun process(block: CommandBlock<C>)
}
suspend inline fun <reified C : Command> CommandStream<C>.send(vararg command: C) = send(C::class, *command)

View File

@@ -0,0 +1,82 @@
package eventDemo.libs.command
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlin.reflect.KClass
typealias CommandBlock<C> = CommandStream.ComputeStatus.(C) -> Unit
/**
* Manage [Command]'s
*
* It stores the new [Command] in memory.
*/
abstract class CommandStreamInMemory<C : Command> : CommandStream<C> {
private val logger = KotlinLogging.logger {}
private val failedCommand = mutableListOf<Command>()
private val queue: Channel<C> = Channel(onUndeliveredElement = { logger.atWarn { "${it.name} elem not send" } })
/**
* Send a new [Command] to the queue.
*/
override suspend fun send(
type: KClass<C>,
command: C,
) {
logger.atInfo {
message = "Command published: $command"
payload = mapOf("command" to command)
}
queue.send(command)
}
override suspend fun process(block: CommandBlock<C>) {
queue.consumeEach { command ->
compute(command, block)
}
for (command in queue) {
compute(command, block)
}
}
private fun compute(
command: C,
block: CommandBlock<C>,
) {
val status = object : CommandStream.ComputeStatus {
var isSet: Boolean = false
override fun ack() {
if (!isSet) markAsSuccess(command) else error("Already NACK")
isSet = true
}
override fun nack() {
if (!isSet) markAsFailed(command) else error("Already ACK")
isSet = true
}
}
if (runCatching { status.block(command) }.isFailure) {
markAsFailed(command)
} else if (!status.isSet) {
status.ack()
}
}
private fun <C : Command> markAsSuccess(command: C) {
logger.atInfo {
message = "Compute command SUCCESS and it removed of the stack : $command"
payload = mapOf("command" to command)
}
}
private fun <C : Command> markAsFailed(command: C) {
failedCommand.add(command)
logger.atWarn {
message = "Compute command FAILD and it put on the top of the stack : $command"
payload = mapOf("command" to command)
}
}
}

View File

@@ -0,0 +1,11 @@
package eventDemo.libs.event
import java.util.UUID
interface AggregateId {
val id: UUID
}
interface Event<ID : AggregateId> {
val id: ID
}

View File

@@ -0,0 +1,19 @@
package eventDemo.libs.event
import kotlinx.coroutines.flow.Flow
import kotlin.reflect.KClass
interface EventStream<E : Event<ID>, ID : AggregateId> {
fun publish(event: E)
fun publish(vararg events: E)
fun readLast(aggregateId: ID): E?
fun <R : E> readLastOf(
aggregateId: ID,
eventType: KClass<out R>,
): E?
fun readAll(aggregateId: ID): Flow<E>
}

View File

@@ -0,0 +1,43 @@
package eventDemo.libs.event
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlin.reflect.KClass
abstract class EventStreamInMemory<E : Event<ID>, ID : AggregateId>(
private val eventType: Class<E>,
) : EventStream<E, ID> {
private val logger = KotlinLogging.logger {}
private val eventBus: MutableList<E> = mutableListOf()
override fun publish(event: E) {
eventBus.add(event)
logger.atInfo {
message = "Event published: $event"
payload = mapOf("event" to event)
}
}
override fun publish(vararg events: E) {
events.forEach { publish(it) }
}
override fun readLast(aggregateId: ID): E? = eventBus.lastOrNull()
override fun <R : E> readLastOf(
aggregateId: ID,
eventType: KClass<out R>,
): R? =
eventBus
.filterIsInstance(eventType.java)
.lastOrNull { it.id == aggregateId }
override fun readAll(aggregateId: ID): Flow<E> =
flow {
eventBus.forEach { emit(it) }
}
}
inline fun <reified R : E, E : Event<ID>, ID : AggregateId> EventStreamInMemory<E, ID>.readLastOf(aggregateId: ID): R? =
readLastOf(aggregateId, R::class)

View File

@@ -0,0 +1,9 @@
package eventDemo.plugins
import eventDemo.app.actions.playNewCard.PlayCardCommandHandler
import io.ktor.server.application.Application
import org.koin.java.KoinJavaComponent.getKoin
fun Application.configureCommandHandler() {
getKoin().get<PlayCardCommandHandler>()()
}

View File

@@ -1,8 +1,8 @@
package eventDemo.plugins package eventDemo.plugins
import eventDemo.app.CommandStream import eventDemo.app.actions.playNewCard.PlayCardCommandHandler
import eventDemo.app.EventStream import eventDemo.shared.command.GameCommandStream
import eventDemo.app.GameId import eventDemo.shared.event.GameEventStream
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.application.install import io.ktor.server.application.install
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
@@ -19,6 +19,7 @@ fun Application.configureKoin() {
val appModule = val appModule =
module { module {
singleOf<EventStream<GameId>>(::EventStream) singleOf<GameEventStream>(::GameEventStream)
singleOf<CommandStream>(::CommandStream) singleOf<GameCommandStream>(::GameCommandStream)
singleOf(::PlayCardCommandHandler)
} }

View File

@@ -1,7 +1,7 @@
package eventDemo.plugins package eventDemo.plugins
import eventDemo.app.actions.card import eventDemo.app.actions.playNewCard.playNewCard
import eventDemo.app.actions.command import eventDemo.app.actions.readLastPlayedCard.readLastPlayedCard
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.application.install import io.ktor.server.application.install
@@ -24,7 +24,7 @@ fun Application.configureRouting() {
} }
routing { routing {
card() playNewCard()
command() readLastPlayedCard()
} }
} }

View File

@@ -1,7 +1,7 @@
package eventDemo.plugins package eventDemo.plugins
import eventDemo.app.CommandId import eventDemo.libs.command.CommandId
import eventDemo.app.GameId import eventDemo.shared.GameId
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.application.install import io.ktor.server.application.install

View File

@@ -1,13 +1,10 @@
package eventDemo.app package eventDemo.shared
import eventDemo.libs.event.AggregateId
import eventDemo.plugins.GameIdSerializer import eventDemo.plugins.GameIdSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import java.util.UUID
sealed interface AggregateId {
val id: UUID
}
@JvmInline @JvmInline
@Serializable(with = GameIdSerializer::class) @Serializable(with = GameIdSerializer::class)
value class GameId( value class GameId(

View File

@@ -0,0 +1,6 @@
package eventDemo.shared.command
import eventDemo.app.actions.playNewCard.PlayCardCommand
import eventDemo.libs.command.CommandStreamInMemory
class GameCommandStream : CommandStreamInMemory<PlayCardCommand>()

View File

@@ -0,0 +1,54 @@
package eventDemo.shared.entity
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 NumericCard(
val number: Int,
val color: Color,
) : Card
sealed interface Special : Card
@Serializable
@SerialName("Reverse")
data class ReverseCard(
val color: Color,
) : Special
@Serializable
@SerialName("Pass")
data class PassCard(
val color: Color,
) : Special
@Serializable
@SerialName("Plus2")
data class Plus2Card(
val color: Color,
) : Special
@Serializable
@SerialName("Plus4")
data class Plus4Card(
val nextColor: Color,
) : Special
@Serializable
@SerialName("ChangeColor")
data class ChangeColorCard(
val nextColor: Color,
) : Special
}

View File

@@ -0,0 +1,13 @@
package eventDemo.shared.entity
import eventDemo.shared.GameId
import kotlinx.serialization.Serializable
@Serializable
data class Game(
val id: GameId,
) {
companion object {
fun new(): Game = Game(GameId())
}
}

View File

@@ -0,0 +1,14 @@
package eventDemo.shared.event
import eventDemo.libs.event.Event
import eventDemo.shared.GameId
import eventDemo.shared.entity.Card
sealed interface GameEvent : Event<GameId> {
override val id: GameId
}
data class CardIsPlayedEvent(
override val id: GameId,
val card: Card,
) : GameEvent

View File

@@ -0,0 +1,6 @@
package eventDemo.shared.event
import eventDemo.libs.event.EventStreamInMemory
import eventDemo.shared.GameId
class GameEventStream : EventStreamInMemory<GameEvent, GameId>(GameEvent::class.java)

View File

@@ -1,11 +1,10 @@
package eventDemo.app.actions 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.module import eventDemo.module
import eventDemo.shared.GameId
import eventDemo.shared.entity.Card
import eventDemo.shared.event.CardIsPlayedEvent
import eventDemo.shared.event.GameEventStream
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.request.accept import io.ktor.client.request.accept
@@ -30,8 +29,9 @@ class CardTest :
stopKoin() stopKoin()
module() module()
} }
val id = GameId() val id = GameId()
val card: Card = Card.Simple(1, Card.Color.Blue) val card: Card = Card.NumericCard(1, Card.Color.Blue)
httpClient() httpClient()
.post("/game/$id/card") { .post("/game/$id/card") {
contentType(Json) contentType(Json)
@@ -40,8 +40,8 @@ class CardTest :
}.apply { }.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText()) assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
val eventStream = getKoin().get<EventStream<GameId>>() val eventStream = getKoin().get<GameEventStream>()
assertEquals(PlayCardEvent(id, card), eventStream.read<PlayCardEvent, GameId>(id)) assertEquals(CardIsPlayedEvent(id, card), eventStream.readLast(id))
} }
} }
} }
@@ -49,20 +49,22 @@ class CardTest :
test("/game/{id}/card/last") { test("/game/{id}/card/last") {
testApplication { testApplication {
val id = GameId() val id = GameId()
val card: Card = Card.Simple(1, Card.Color.Blue) val card: Card = Card.NumericCard(1, Card.Color.Blue)
application { application {
stopKoin() stopKoin()
module() module()
val eventStream by inject<EventStream<GameId>>() val eventStream by inject<GameEventStream>()
eventStream.publish( eventStream.publish(
PlayCardEvent(GameId(), Card.Simple(2, Card.Color.Yellow)), CardIsPlayedEvent(id, Card.NumericCard(2, Card.Color.Yellow)),
PlayCardEvent(id, card), CardIsPlayedEvent(id, card),
// Other game
CardIsPlayedEvent(GameId(), Card.NumericCard(2, Card.Color.Yellow)),
) )
} }
httpClient().get("/game/$id/card/last").apply { httpClient().get("/game/$id/card/last").apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText()) assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
assertEquals(card, this.call.body<Card>()) assertEquals(card, call.body<Card>())
} }
} }
} }

View File

@@ -1,70 +0,0 @@
package eventDemo.app.actions
import eventDemo.app.Card
import eventDemo.app.Command
import eventDemo.app.CommandStream
import eventDemo.app.Game
import eventDemo.app.PlayCardCommand
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.client.statement.bodyAsText
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.java.KoinJavaComponent.getKoin
import org.koin.ktor.ext.inject
import kotlin.test.assertEquals
class CommandTest :
FunSpec({
test("/command/send") {
testApplication {
val client = httpClient()
application {
stopKoin()
module()
}
val command = PlayCardCommand(Game.new(), Card.Simple(1, Card.Color.Blue))
client
.post("/command/send") {
contentType(Json)
accept(Json)
setBody(command)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
val commandStream = getKoin().get<CommandStream>()
assertEquals(command, commandStream.readNext())
}
}
}
test("/command/next") {
testApplication {
val command =
PlayCardCommand(
Game.new(),
Card.Simple(1, Card.Color.Blue),
)
application {
stopKoin()
module()
val commandStream by inject<CommandStream>()
commandStream.sendRequest(command)
}
httpClient().get("/command/next").apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
assertEquals(command, this.call.body<Command>())
}
}
}
})