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
import eventDemo.configuration.configure
import io.ktor.server.application.Application
import io.ktor.server.engine.embeddedServer
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
import eventDemo.app.actions.playNewCard.PlayCardCommand
import eventDemo.shared.command.GameCommandStream
import eventDemo.shared.entity.Player
import eventDemo.shared.event.CardIsPlayedEvent
import eventDemo.shared.event.GameEvent
import eventDemo.shared.event.GameEventStream
import eventDemo.shared.event.GameState
import eventDemo.shared.event.buildStateFromEventStream
import eventDemo.app.GameState
import eventDemo.app.command.GameCommand
import eventDemo.app.command.GameCommandStream
import eventDemo.app.command.PlayCardCommand
import eventDemo.app.entity.Player
import eventDemo.app.event.CardIsPlayedEvent
import eventDemo.app.event.GameEvent
import eventDemo.app.event.GameEventStream
import eventDemo.app.event.buildStateFromEventStream
import io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -17,7 +18,7 @@ import kotlinx.coroutines.launch
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.
*/
@@ -64,10 +65,10 @@ class GameCommandHandler(
}
}
}
}
private fun GameState.commandCardCanBeExecuted(command: PlayCardCommand): Boolean =
private fun GameState.commandCardCanBeExecuted(command: PlayCardCommand): Boolean =
canBePlayThisCard(
command.payload.player,
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.shared.GameId
import eventDemo.shared.event.GameEvent
import eventDemo.shared.toFrame
import io.ktor.websocket.Frame
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.runBlocking
class GameEventPlayerNotificationSubscriber(
class GameEventPlayerNotificationListener(
private val eventBus: EventBus<GameEvent, GameId>,
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.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 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.plugins.GameIdSerializer
import kotlinx.serialization.Serializable
import java.util.UUID

View File

@@ -1,10 +1,11 @@
package eventDemo.shared.event
package eventDemo.app
import eventDemo.shared.GameId
import eventDemo.shared.entity.Card
import eventDemo.shared.entity.Deck
import eventDemo.shared.entity.Player
import eventDemo.app.entity.Card
import eventDemo.app.entity.Deck
import eventDemo.app.entity.Player
import kotlinx.serialization.Serializable
@Serializable
data class GameState(
val gameId: GameId,
val players: Set<Player> = emptySet(),
@@ -16,6 +17,7 @@ data class GameState(
val deck: Deck = Deck(players.toList()),
val isStarted: Boolean = false,
) {
@Serializable
data class LastCard(
val card: Card,
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.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
@@ -25,6 +26,12 @@ class Game(
class Card(
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.
*/
get<Game.Card> { card ->
get<Game.Card> { body ->
eventStream
.readLastOf<CardIsPlayedEvent, _, _>(card.game.id)
.readLastOf<CardIsPlayedEvent, _, _>(body.game.id)
?.let { call.respond(it.card) }
?: 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.CommandStreamChannel
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.CommandId
import eventDemo.shared.GameId
import eventDemo.shared.entity.Card
import eventDemo.shared.entity.Player
import kotlinx.serialization.SerialName
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.Serializable
import java.util.UUID

View File

@@ -1,4 +1,4 @@
package eventDemo.shared.entity
package eventDemo.app.entity
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.plugins.PlayerIdSerializer
import eventDemo.plugins.UUIDSerializer
import io.ktor.server.auth.Principal
import kotlinx.serialization.Serializable
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.shared.GameId
import eventDemo.shared.entity.Card
import eventDemo.shared.entity.Deck
import eventDemo.shared.entity.Player
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.shared.GameId
class GameEventBus(
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.EventStream
import eventDemo.shared.GameId
/**
* 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.shared.GameId
import eventDemo.shared.entity.Card
fun GameId.buildStateFromEventStream(eventStream: EventStream<GameEvent, GameId>): GameState =
buildStateFromEvents(
@@ -61,7 +62,7 @@ private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
is GameStartedEvent -> {
state.copy(
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,
deck = event.deck,
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.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.EventStreamInMemory
import eventDemo.shared.event.GameEventBus
import eventDemo.shared.event.GameEventStream
import io.ktor.server.application.Application
import io.ktor.server.application.install
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.readLastPlayedCard.readLastPlayedCard
import eventDemo.app.actions.readGameState
import eventDemo.app.actions.readLastPlayedCard
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.install
@@ -24,7 +24,7 @@ fun Application.configureHttpRouting() {
}
routing {
playNewCard()
readLastPlayedCard()
readGameState()
}
}

View File

@@ -1,4 +1,4 @@
package eventDemo.plugins
package eventDemo.configuration
import com.auth0.jwt.JWT
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.jwt
import io.ktor.server.response.respond
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.routing
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.shared.GameId
import eventDemo.shared.entity.Player.PlayerId
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
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.GameEventPlayerNotificationSubscriber
import eventDemo.shared.entity.Player
import eventDemo.shared.event.GameEventBus
import eventDemo.shared.event.GameEventStream
import eventDemo.app.entity.Player
import eventDemo.app.event.GameEventBus
import eventDemo.app.event.GameEventStream
import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.install
@@ -35,7 +35,7 @@ fun Application.configureWebSocketsGameRoute(
authenticate {
webSocket("/game") {
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
import eventDemo.plugins.CommandIdSerializer
import eventDemo.configuration.CommandIdSerializer
import kotlinx.serialization.Serializable
import java.util.UUID

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
package eventDemo.app.actions
import eventDemo.configure
import eventDemo.shared.GameId
import eventDemo.shared.entity.Card
import eventDemo.shared.entity.Player
import eventDemo.shared.event.CardIsPlayedEvent
import eventDemo.shared.event.GameEventStream
import eventDemo.app.GameId
import eventDemo.app.entity.Card
import eventDemo.app.entity.Player
import eventDemo.app.event.CardIsPlayedEvent
import eventDemo.app.event.GameEventStream
import eventDemo.configuration.configure
import io.kotest.core.spec.style.FunSpec
import io.ktor.client.call.body
import io.ktor.client.request.accept

View File

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