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

@@ -1,19 +0,0 @@
package eventDemo.app
import eventDemo.plugins.GameIdSerializer
import kotlinx.serialization.Serializable
import java.util.UUID
sealed interface AggregateId {
val id: UUID
}
@JvmInline
@Serializable(with = GameIdSerializer::class)
value class GameId(
override val id: UUID = UUID.randomUUID(),
) : AggregateId {
constructor(id: String) : this(UUID.fromString(id))
override fun toString(): String = id.toString()
}

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.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
@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)
}
}