Refactor
This commit is contained in:
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user