refactoring

This commit is contained in:
2025-03-04 23:21:06 +01:00
parent f3ca94c97e
commit 06443d7efa
31 changed files with 140 additions and 185 deletions

View File

@@ -1,5 +1,6 @@
package eventDemo package eventDemo
import eventDemo.configuration.configure
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty

View File

@@ -1,29 +0,0 @@
package eventDemo
import eventDemo.app.actions.GameEventReactionSubscriber
import eventDemo.plugins.configureHttp
import eventDemo.plugins.configureHttpRouting
import eventDemo.plugins.configureKoin
import eventDemo.plugins.configureSecurity
import eventDemo.plugins.configureSerialization
import eventDemo.plugins.configureSockets
import eventDemo.plugins.configureWebSocketsGameRoute
import io.ktor.server.application.Application
import org.koin.ktor.ext.get
fun Application.configure() {
configureKoin()
configureSecurity()
configureSerialization()
configureSockets()
configureWebSocketsGameRoute(get(), get())
configureHttp()
configureHttpRouting()
GameEventReactionSubscriber(get(), get())
.init()
}

View File

@@ -1,13 +1,14 @@
package eventDemo.app.actions package eventDemo.app.actions
import eventDemo.app.actions.playNewCard.PlayCardCommand import eventDemo.app.GameState
import eventDemo.shared.command.GameCommandStream import eventDemo.app.command.GameCommand
import eventDemo.shared.entity.Player import eventDemo.app.command.GameCommandStream
import eventDemo.shared.event.CardIsPlayedEvent import eventDemo.app.command.PlayCardCommand
import eventDemo.shared.event.GameEvent import eventDemo.app.entity.Player
import eventDemo.shared.event.GameEventStream import eventDemo.app.event.CardIsPlayedEvent
import eventDemo.shared.event.GameState import eventDemo.app.event.GameEvent
import eventDemo.shared.event.buildStateFromEventStream import eventDemo.app.event.GameEventStream
import eventDemo.app.event.buildStateFromEventStream
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -17,7 +18,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
/** /**
* Listen [PlayCardCommand] on [GameCommandStream], check the validity and execute the action. * Listen [GameCommand] on [GameCommandStream], check the validity and execute an action.
* *
* This action can be executing an action and produce a new [GameEvent] after verification. * This action can be executing an action and produce a new [GameEvent] after verification.
*/ */
@@ -64,10 +65,10 @@ class GameCommandHandler(
} }
} }
} }
}
private fun GameState.commandCardCanBeExecuted(command: PlayCardCommand): Boolean = private fun GameState.commandCardCanBeExecuted(command: PlayCardCommand): Boolean =
canBePlayThisCard( canBePlayThisCard(
command.payload.player, command.payload.player,
command.payload.card, command.payload.card,
) )
}

View File

@@ -1,14 +1,13 @@
package eventDemo.app.actions package eventDemo.app
import eventDemo.app.event.GameEvent
import eventDemo.libs.event.EventBus import eventDemo.libs.event.EventBus
import eventDemo.shared.GameId
import eventDemo.shared.event.GameEvent
import eventDemo.shared.toFrame import eventDemo.shared.toFrame
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class GameEventPlayerNotificationSubscriber( class GameEventPlayerNotificationListener(
private val eventBus: EventBus<GameEvent, GameId>, private val eventBus: EventBus<GameEvent, GameId>,
private val outgoing: SendChannel<Frame>, private val outgoing: SendChannel<Frame>,
) { ) {

View File

@@ -1,13 +1,12 @@
package eventDemo.app.actions package eventDemo.app
import eventDemo.app.event.GameEvent
import eventDemo.app.event.GameStartedEvent
import eventDemo.app.event.buildStateFromEventStream
import eventDemo.libs.event.EventBus import eventDemo.libs.event.EventBus
import eventDemo.libs.event.EventStream import eventDemo.libs.event.EventStream
import eventDemo.shared.GameId
import eventDemo.shared.event.GameEvent
import eventDemo.shared.event.GameStartedEvent
import eventDemo.shared.event.buildStateFromEventStream
class GameEventReactionSubscriber( class GameEventReactionListener(
private val eventBus: EventBus<GameEvent, GameId>, private val eventBus: EventBus<GameEvent, GameId>,
private val eventStream: EventStream<GameEvent, GameId>, private val eventStream: EventStream<GameEvent, GameId>,
) { ) {

View File

@@ -1,7 +1,7 @@
package eventDemo.shared package eventDemo.app
import eventDemo.configuration.GameIdSerializer
import eventDemo.libs.event.AggregateId import eventDemo.libs.event.AggregateId
import eventDemo.plugins.GameIdSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import java.util.UUID

View File

@@ -1,10 +1,11 @@
package eventDemo.shared.event package eventDemo.app
import eventDemo.shared.GameId import eventDemo.app.entity.Card
import eventDemo.shared.entity.Card import eventDemo.app.entity.Deck
import eventDemo.shared.entity.Deck import eventDemo.app.entity.Player
import eventDemo.shared.entity.Player import kotlinx.serialization.Serializable
@Serializable
data class GameState( data class GameState(
val gameId: GameId, val gameId: GameId,
val players: Set<Player> = emptySet(), val players: Set<Player> = emptySet(),
@@ -16,6 +17,7 @@ data class GameState(
val deck: Deck = Deck(players.toList()), val deck: Deck = Deck(players.toList()),
val isStarted: Boolean = false, val isStarted: Boolean = false,
) { ) {
@Serializable
data class LastCard( data class LastCard(
val card: Card, val card: Card,
val player: Player, val player: Player,

View File

@@ -1,10 +1,11 @@
package eventDemo.app.actions.readLastPlayedCard package eventDemo.app.actions
import eventDemo.app.GameId
import eventDemo.app.event.CardIsPlayedEvent
import eventDemo.app.event.GameEventStream
import eventDemo.app.event.buildStateFromEventStream
import eventDemo.configuration.GameIdSerializer
import eventDemo.libs.event.readLastOf 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.http.HttpStatusCode
import io.ktor.resources.Resource import io.ktor.resources.Resource
import io.ktor.server.application.call import io.ktor.server.application.call
@@ -25,6 +26,12 @@ class Game(
class Card( class Card(
val game: Game, val game: Game,
) )
@Serializable
@Resource("state")
class State(
val game: Game,
)
} }
/** /**
@@ -36,10 +43,25 @@ fun Routing.readLastPlayedCard() {
/* /*
* Read the last played card on the game. * Read the last played card on the game.
*/ */
get<Game.Card> { card -> get<Game.Card> { body ->
eventStream eventStream
.readLastOf<CardIsPlayedEvent, _, _>(card.game.id) .readLastOf<CardIsPlayedEvent, _, _>(body.game.id)
?.let { call.respond(it.card) } ?.let { call.respond(it.card) }
?: call.response.status(HttpStatusCode.BadRequest) ?: call.response.status(HttpStatusCode.BadRequest)
} }
} }
/**
* API route to read the last card played.
*/
fun Routing.readGameState() {
val eventStream by inject<GameEventStream>()
/*
* Read the last played card on the game.
*/
get<Game.State> { body ->
val state = body.game.id.buildStateFromEventStream(eventStream)
call.respond(state)
}
}

View File

@@ -1,61 +0,0 @@
package eventDemo.app.actions.playNewCard
import eventDemo.libs.command.send
import eventDemo.shared.GameId
import eventDemo.shared.command.GameCommandStreamInMemory
import eventDemo.shared.entity.Card
import eventDemo.shared.entity.Player
import io.ktor.http.HttpStatusCode
import io.ktor.resources.Resource
import io.ktor.server.application.call
import io.ktor.server.auth.authenticate
import io.ktor.server.auth.principal
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
@Serializable
@Resource("/game/{id}")
class GameRoute(
// @Serializable(with = GameIdSerializer::class)
val id: GameId,
) {
@Serializable
@Resource("card")
class Card(
val game: GameRoute,
)
}
/**
* API route to send a request to play card.
*/
fun Routing.playNewCard() {
val commandStream = GameCommandStreamInMemory()
authenticate {
/*
* 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>()
val name = call.principal<Player>()!!
launch(Dispatchers.Default) {
commandStream.send(
PlayCardCommand(
it.game.id,
name,
card,
),
)
}
call.respondNullable<Any?>(HttpStatusCode.OK, null)
}
}
}

View File

@@ -1,6 +1,5 @@
package eventDemo.shared.command package eventDemo.app.command
import eventDemo.app.actions.playNewCard.GameCommand
import eventDemo.libs.command.CommandStream import eventDemo.libs.command.CommandStream
import eventDemo.libs.command.CommandStreamChannel import eventDemo.libs.command.CommandStreamChannel
import eventDemo.libs.command.CommandStreamInMemory import eventDemo.libs.command.CommandStreamInMemory

View File

@@ -1,10 +1,10 @@
package eventDemo.app.actions.playNewCard package eventDemo.app.command
import eventDemo.app.GameId
import eventDemo.app.entity.Card
import eventDemo.app.entity.Player
import eventDemo.libs.command.Command import eventDemo.libs.command.Command
import eventDemo.libs.command.CommandId import eventDemo.libs.command.CommandId
import eventDemo.shared.GameId
import eventDemo.shared.entity.Card
import eventDemo.shared.entity.Player
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@@ -1,6 +1,6 @@
package eventDemo.shared.entity package eventDemo.app.entity
import eventDemo.plugins.UUIDSerializer import eventDemo.configuration.UUIDSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import java.util.UUID

View File

@@ -1,4 +1,4 @@
package eventDemo.shared.entity package eventDemo.app.entity
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@@ -1,8 +1,8 @@
package eventDemo.shared.entity package eventDemo.app.entity
import eventDemo.configuration.PlayerIdSerializer
import eventDemo.configuration.UUIDSerializer
import eventDemo.libs.event.AggregateId import eventDemo.libs.event.AggregateId
import eventDemo.plugins.PlayerIdSerializer
import eventDemo.plugins.UUIDSerializer
import io.ktor.server.auth.Principal import io.ktor.server.auth.Principal
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import java.util.UUID

View File

@@ -1,10 +1,10 @@
package eventDemo.shared.event package eventDemo.app.event
import eventDemo.app.GameId
import eventDemo.app.entity.Card
import eventDemo.app.entity.Deck
import eventDemo.app.entity.Player
import eventDemo.libs.event.Event import eventDemo.libs.event.Event
import eventDemo.shared.GameId
import eventDemo.shared.entity.Card
import eventDemo.shared.entity.Deck
import eventDemo.shared.entity.Player
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/** /**

View File

@@ -1,7 +1,7 @@
package eventDemo.shared.event package eventDemo.app.event
import eventDemo.app.GameId
import eventDemo.libs.event.EventBus import eventDemo.libs.event.EventBus
import eventDemo.shared.GameId
class GameEventBus( class GameEventBus(
bus: EventBus<GameEvent, GameId>, bus: EventBus<GameEvent, GameId>,

View File

@@ -1,8 +1,8 @@
package eventDemo.shared.event package eventDemo.app.event
import eventDemo.app.GameId
import eventDemo.libs.event.EventBus import eventDemo.libs.event.EventBus
import eventDemo.libs.event.EventStream import eventDemo.libs.event.EventStream
import eventDemo.shared.GameId
/** /**
* A stream to publish and read the played card event. * A stream to publish and read the played card event.

View File

@@ -1,8 +1,9 @@
package eventDemo.shared.event package eventDemo.app.event
import eventDemo.app.GameId
import eventDemo.app.GameState
import eventDemo.app.entity.Card
import eventDemo.libs.event.EventStream import eventDemo.libs.event.EventStream
import eventDemo.shared.GameId
import eventDemo.shared.entity.Card
fun GameId.buildStateFromEventStream(eventStream: EventStream<GameEvent, GameId>): GameState = fun GameId.buildStateFromEventStream(eventStream: EventStream<GameEvent, GameId>): GameState =
buildStateFromEvents( buildStateFromEvents(
@@ -61,7 +62,7 @@ private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
is GameStartedEvent -> { is GameStartedEvent -> {
state.copy( state.copy(
lastColor = (event.deck.discard.first() as? Card.ColorCard)?.color, lastColor = (event.deck.discard.first() as? Card.ColorCard)?.color,
lastCard = GameState.LastCard(event.deck.discard.first(), event.firstPlayer), lastCard = eventDemo.app.GameState.LastCard(event.deck.discard.first(), event.firstPlayer),
lastPlayer = event.firstPlayer, lastPlayer = event.firstPlayer,
deck = event.deck, deck = event.deck,
isStarted = true, isStarted = true,

View File

@@ -0,0 +1,22 @@
package eventDemo.configuration
import eventDemo.app.GameEventReactionListener
import io.ktor.server.application.Application
import org.koin.ktor.ext.get
fun Application.configure() {
configureKoin()
configureSecurity()
configureSerialization()
configureSockets()
configureWebSocketsGameRoute(get(), get())
configureHttp()
configureHttpRouting()
GameEventReactionListener(get(), get())
.init()
}

View File

@@ -1,4 +1,4 @@
package eventDemo.plugins package eventDemo.configuration
import io.ktor.http.HttpHeaders import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod

View File

@@ -1,9 +1,9 @@
package eventDemo.plugins package eventDemo.configuration
import eventDemo.app.event.GameEventBus
import eventDemo.app.event.GameEventStream
import eventDemo.libs.event.EventBusInMemory import eventDemo.libs.event.EventBusInMemory
import eventDemo.libs.event.EventStreamInMemory import eventDemo.libs.event.EventStreamInMemory
import eventDemo.shared.event.GameEventBus
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.dsl.module import org.koin.dsl.module

View File

@@ -1,7 +1,7 @@
package eventDemo.plugins package eventDemo.configuration
import eventDemo.app.actions.playNewCard.playNewCard import eventDemo.app.actions.readGameState
import eventDemo.app.actions.readLastPlayedCard.readLastPlayedCard import eventDemo.app.actions.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.configureHttpRouting() {
} }
routing { routing {
playNewCard()
readLastPlayedCard() readLastPlayedCard()
readGameState()
} }
} }

View File

@@ -1,4 +1,4 @@
package eventDemo.plugins package eventDemo.configuration
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
@@ -9,7 +9,6 @@ import io.ktor.server.auth.authentication
import io.ktor.server.auth.jwt.JWTPrincipal import io.ktor.server.auth.jwt.JWTPrincipal
import io.ktor.server.auth.jwt.jwt import io.ktor.server.auth.jwt.jwt
import io.ktor.server.response.respond import io.ktor.server.response.respond
import io.ktor.server.routing.get
import io.ktor.server.routing.post import io.ktor.server.routing.post
import io.ktor.server.routing.routing import io.ktor.server.routing.routing
import java.util.Date import java.util.Date

View File

@@ -1,8 +1,8 @@
package eventDemo.plugins package eventDemo.configuration
import eventDemo.app.GameId
import eventDemo.app.entity.Player.PlayerId
import eventDemo.libs.command.CommandId import eventDemo.libs.command.CommandId
import eventDemo.shared.GameId
import eventDemo.shared.entity.Player.PlayerId
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,10 +1,10 @@
package eventDemo.plugins package eventDemo.configuration
import eventDemo.app.GameEventPlayerNotificationListener
import eventDemo.app.actions.GameCommandHandler import eventDemo.app.actions.GameCommandHandler
import eventDemo.app.actions.GameEventPlayerNotificationSubscriber import eventDemo.app.entity.Player
import eventDemo.shared.entity.Player import eventDemo.app.event.GameEventBus
import eventDemo.shared.event.GameEventBus import eventDemo.app.event.GameEventStream
import eventDemo.shared.event.GameEventStream
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationCall import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.install import io.ktor.server.application.install
@@ -35,7 +35,7 @@ fun Application.configureWebSocketsGameRoute(
authenticate { authenticate {
webSocket("/game") { webSocket("/game") {
GameCommandHandler(eventStream, incoming, outgoing).init(call.getPlayer()) GameCommandHandler(eventStream, incoming, outgoing).init(call.getPlayer())
GameEventPlayerNotificationSubscriber(eventBus, outgoing).init() GameEventPlayerNotificationListener(eventBus, outgoing).init()
} }
} }
} }

View File

@@ -1,6 +1,6 @@
package eventDemo.libs.command package eventDemo.libs.command
import eventDemo.plugins.CommandIdSerializer import eventDemo.configuration.CommandIdSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import java.util.UUID

View File

@@ -19,7 +19,7 @@ interface EventStream<E : Event<ID>, ID : AggregateId> {
fun <R : E> readLastOf( fun <R : E> readLastOf(
aggregateId: ID, aggregateId: ID,
eventType: KClass<out R>, eventType: KClass<out R>,
): E? ): R?
/** Reads all events associated with a given aggregate ID */ /** Reads all events associated with a given aggregate ID */
fun readAll(aggregateId: ID): List<E> fun readAll(aggregateId: ID): List<E>

View File

@@ -37,5 +37,5 @@ class EventStreamInMemory<E : Event<ID>, ID : AggregateId> : EventStream<E, ID>
override fun readAll(aggregateId: ID): List<E> = events override fun readAll(aggregateId: ID): List<E> = events
} }
inline fun <reified R : E, E : Event<ID>, ID : AggregateId> EventStreamInMemory<E, ID>.readLastOf(aggregateId: ID): R? = inline fun <reified R : E, E : Event<ID>, ID : AggregateId> EventStream<E, ID>.readLastOf(aggregateId: ID): R? =
readLastOf(aggregateId, R::class) readLastOf(aggregateId, R::class)

View File

@@ -1,7 +1,7 @@
package eventDemo.shared package eventDemo.shared
import eventDemo.app.actions.playNewCard.GameCommand import eventDemo.app.command.GameCommand
import eventDemo.shared.event.GameEvent import eventDemo.app.event.GameEvent
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import io.ktor.websocket.readText import io.ktor.websocket.readText
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json

View File

@@ -1,11 +1,11 @@
package eventDemo.app.actions package eventDemo.app.actions
import eventDemo.configure import eventDemo.app.GameId
import eventDemo.shared.GameId import eventDemo.app.entity.Card
import eventDemo.shared.entity.Card import eventDemo.app.entity.Player
import eventDemo.shared.entity.Player import eventDemo.app.event.CardIsPlayedEvent
import eventDemo.shared.event.CardIsPlayedEvent import eventDemo.app.event.GameEventStream
import eventDemo.shared.event.GameEventStream import eventDemo.configuration.configure
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

View File

@@ -1,6 +1,6 @@
package eventDemo.app.actions package eventDemo.app.actions
import eventDemo.plugins.UUIDSerializer import eventDemo.configuration.UUIDSerializer
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json