add IamReadyToPlayCommand & refactoring

This commit is contained in:
2025-03-05 00:07:41 +01:00
parent 06443d7efa
commit bc35131bfc
41 changed files with 404 additions and 322 deletions

View File

@@ -2,6 +2,7 @@ package eventDemo.app
import eventDemo.app.entity.Card
import eventDemo.app.entity.Deck
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player
import kotlinx.serialization.Serializable

View File

@@ -1,67 +0,0 @@
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 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,
)
@Serializable
@Resource("state")
class State(
val game: Game,
)
}
/**
* API route to read the last card played.
*/
fun Routing.readLastPlayedCard() {
val eventStream by inject<GameEventStream>()
/*
* Read the last played card on the game.
*/
get<Game.Card> { body ->
eventStream
.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,14 +1,15 @@
package eventDemo.app.actions
package eventDemo.app.command
import eventDemo.app.GameState
import eventDemo.app.command.GameCommand
import eventDemo.app.command.GameCommandStream
import eventDemo.app.command.PlayCardCommand
import eventDemo.app.command.command.GameCommand
import eventDemo.app.command.command.IamReadyToPlayCommand
import eventDemo.app.command.command.IwantToPlayCardCommand
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 eventDemo.app.event.event.CardIsPlayedEvent
import eventDemo.app.event.event.GameEvent
import eventDemo.app.event.event.PlayerReadyEvent
import io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -35,24 +36,22 @@ class GameCommandHandler(
*/
fun init(player: Player) {
CoroutineScope(Dispatchers.IO).launch {
commandStream.process {
if (it.payload.player.id != player.id) {
commandStream.process { command ->
if (command.payload.player.id != player.id) {
nack()
}
when (it) {
is PlayCardCommand -> {
// Check the command can be executed
val canBeExecuted =
it.payload.gameId
.buildStateFromEventStream(eventStream)
.commandCardCanBeExecuted(it)
if (canBeExecuted) {
val state = command.buildState()
when (command) {
is IwantToPlayCardCommand -> {
// Check the command can be executed
if (state.commandCardCanBeExecuted(command)) {
eventStream.publish(
CardIsPlayedEvent(
it.payload.gameId,
it.payload.card,
it.payload.player,
command.payload.gameId,
command.payload.card,
command.payload.player,
),
)
} else {
@@ -61,14 +60,29 @@ class GameCommandHandler(
}
}
}
is IamReadyToPlayCommand -> {
if (state.playerIsAlreadyReady(command.payload.player)) {
nack()
} else {
PlayerReadyEvent(
command.payload.gameId,
command.payload.player,
)
}
}
}
}
}
}
private fun GameState.commandCardCanBeExecuted(command: PlayCardCommand): Boolean =
private fun GameState.playerIsAlreadyReady(player: Player): Boolean = readyPlayers.contains(player)
private fun GameState.commandCardCanBeExecuted(command: IwantToPlayCardCommand): Boolean =
canBePlayThisCard(
command.payload.player,
command.payload.card,
)
private fun GameCommand.buildState(): GameState = payload.gameId.buildStateFromEventStream(eventStream)
}

View File

@@ -0,0 +1,32 @@
package eventDemo.app.command
import eventDemo.app.entity.Player
import eventDemo.app.event.GameEventBus
import eventDemo.app.event.GameEventStream
import eventDemo.app.eventListener.GameEventPlayerNotificationListener
import io.ktor.server.application.ApplicationCall
import io.ktor.server.auth.authenticate
import io.ktor.server.auth.jwt.JWTPrincipal
import io.ktor.server.auth.principal
import io.ktor.server.routing.Route
import io.ktor.server.websocket.webSocket
fun Route.gameSocket(
eventStream: GameEventStream,
eventBus: GameEventBus,
) {
authenticate {
webSocket("/game") {
GameCommandHandler(eventStream, incoming, outgoing).init(call.getPlayer())
GameEventPlayerNotificationListener(eventBus, outgoing).init()
}
}
}
private fun ApplicationCall.getPlayer() =
principal<JWTPrincipal>()!!.run {
Player(
id = payload.getClaim("playerid").asString(),
name = payload.getClaim("username").asString(),
)
}

View File

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

View File

@@ -0,0 +1,17 @@
package eventDemo.app.command.command
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player
import eventDemo.libs.command.Command
import kotlinx.serialization.Serializable
@Serializable
sealed interface GameCommand : Command {
val payload: Payload
@Serializable
sealed interface Payload {
val gameId: GameId
val player: Player
}
}

View File

@@ -0,0 +1,25 @@
package eventDemo.app.command.command
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player
import eventDemo.libs.command.CommandId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* A command to set as ready to play
*/
@Serializable
@SerialName("Ready")
data class IamReadyToPlayCommand(
override val payload: Payload,
) : GameCommand {
override val name: String = "Ready"
override val id: CommandId = CommandId()
@Serializable
data class Payload(
override val gameId: GameId,
override val player: Player,
) : GameCommand.Payload
}

View File

@@ -1,9 +1,8 @@
package eventDemo.app.command
package eventDemo.app.command.command
import eventDemo.app.GameId
import eventDemo.app.entity.Card
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player
import eventDemo.libs.command.Command
import eventDemo.libs.command.CommandId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -13,15 +12,9 @@ import kotlinx.serialization.Serializable
*/
@Serializable
@SerialName("PlayCard")
data class PlayCardCommand(
data class IwantToPlayCardCommand(
override val payload: Payload,
) : GameCommand {
constructor(
gameId: GameId,
player: Player,
card: Card,
) : this(Payload(gameId, player, card))
override val name: String = "PlayCard"
override val id: CommandId = CommandId()
@@ -32,14 +25,3 @@ data class PlayCardCommand(
val card: Card,
) : GameCommand.Payload
}
@Serializable
sealed interface GameCommand : Command {
val payload: Payload
@Serializable
sealed interface Payload {
val gameId: GameId
val player: Player
}
}

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
package eventDemo.app.entity
import eventDemo.configuration.PlayerIdSerializer
import eventDemo.configuration.UUIDSerializer
import eventDemo.libs.event.AggregateId
import eventDemo.shared.PlayerIdSerializer
import eventDemo.shared.UUIDSerializer
import io.ktor.server.auth.Principal
import kotlinx.serialization.Serializable
import java.util.UUID
@@ -26,11 +26,3 @@ data class Player(
override fun toString(): String = id.toString()
}
}
@Serializable
data class PlayerHand(
val player: Player,
val cards: List<Card> = emptyList(),
) {
val count = lazy { cards.count() }
}

View File

@@ -0,0 +1,11 @@
package eventDemo.app.entity
import kotlinx.serialization.Serializable
@Serializable
data class PlayerHand(
val player: Player,
val cards: List<Card> = emptyList(),
) {
val count = lazy { cards.count() }
}

View File

@@ -1,79 +0,0 @@
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 kotlinx.serialization.Serializable
/**
* An [Event] of a Game.
*/
@Serializable
sealed interface GameEvent : Event<GameId> {
override val id: GameId
}
/**
* An [Event] to represent a played card.
*/
data class CardIsPlayedEvent(
override val id: GameId,
val card: Card,
val player: Player,
) : GameEvent
/**
* An [Event] to represent a new player joining the game.
*/
data class NewPlayerEvent(
override val id: GameId,
val player: Player,
) : GameEvent
/**
* This [Event] is sent when a player is ready.
*/
data class PlayerReadyEvent(
override val id: GameId,
val player: Player,
) : GameEvent
/**
* This [Event] is sent when a player is ready.
*/
data class GameStartedEvent(
override val id: GameId,
val firstPlayer: Player,
val deck: Deck,
) : GameEvent {
companion object {
fun new(
id: GameId,
players: Set<Player>,
): GameStartedEvent =
GameStartedEvent(
id = id,
firstPlayer = players.random(),
deck = Deck.initHands(players).putOneCardOnDiscard(),
)
}
}
/**
* This [Event] is sent when a player can play.
*/
data class PlayerHavePassEvent(
override val id: GameId,
val player: Player,
) : GameEvent
/**
* This [Event] is sent when a player chose a color.
*/
data class PlayerChoseColorEvent(
override val id: GameId,
val player: Player,
val color: Card.Color,
) : GameEvent

View File

@@ -1,6 +1,7 @@
package eventDemo.app.event
import eventDemo.app.GameId
import eventDemo.app.entity.GameId
import eventDemo.app.event.event.GameEvent
import eventDemo.libs.event.EventBus
class GameEventBus(

View File

@@ -1,6 +1,7 @@
package eventDemo.app.event
import eventDemo.app.GameId
import eventDemo.app.entity.GameId
import eventDemo.app.event.event.GameEvent
import eventDemo.libs.event.EventBus
import eventDemo.libs.event.EventStream

View File

@@ -1,8 +1,15 @@
package eventDemo.app.event
import eventDemo.app.GameId
import eventDemo.app.GameState
import eventDemo.app.entity.Card
import eventDemo.app.entity.GameId
import eventDemo.app.event.event.CardIsPlayedEvent
import eventDemo.app.event.event.GameEvent
import eventDemo.app.event.event.GameStartedEvent
import eventDemo.app.event.event.NewPlayerEvent
import eventDemo.app.event.event.PlayerChoseColorEvent
import eventDemo.app.event.event.PlayerHavePassEvent
import eventDemo.app.event.event.PlayerReadyEvent
import eventDemo.libs.event.EventStream
fun GameId.buildStateFromEventStream(eventStream: EventStream<GameEvent, GameId>): GameState =

View File

@@ -0,0 +1,14 @@
package eventDemo.app.event.event
import eventDemo.app.entity.Card
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player
/**
* An [GameEvent] to represent a played card.
*/
data class CardIsPlayedEvent(
override val id: GameId,
val card: Card,
val player: Player,
) : GameEvent

View File

@@ -0,0 +1,13 @@
package eventDemo.app.event.event
import eventDemo.app.entity.GameId
import eventDemo.libs.event.Event
import kotlinx.serialization.Serializable
/**
* An [Event] of a Game.
*/
@Serializable
sealed interface GameEvent : Event<GameId> {
override val id: GameId
}

View File

@@ -0,0 +1,26 @@
package eventDemo.app.event.event
import eventDemo.app.entity.Deck
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player
/**
* This [GameEvent] is sent when all players is ready.
*/
data class GameStartedEvent(
override val id: GameId,
val firstPlayer: Player,
val deck: Deck,
) : GameEvent {
companion object {
fun new(
id: GameId,
players: Set<Player>,
): GameStartedEvent =
GameStartedEvent(
id = id,
firstPlayer = players.random(),
deck = Deck.initHands(players).putOneCardOnDiscard(),
)
}
}

View File

@@ -0,0 +1,12 @@
package eventDemo.app.event.event
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player
/**
* An [GameEvent] to represent a new player joining the game.
*/
data class NewPlayerEvent(
override val id: GameId,
val player: Player,
) : GameEvent

View File

@@ -0,0 +1,14 @@
package eventDemo.app.event.event
import eventDemo.app.entity.Card
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player
/**
* This [GameEvent] is sent when a player chose a color.
*/
data class PlayerChoseColorEvent(
override val id: GameId,
val player: Player,
val color: Card.Color,
) : GameEvent

View File

@@ -0,0 +1,12 @@
package eventDemo.app.event.event
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player
/**
* This [GameEvent] is sent when a player can play.
*/
data class PlayerHavePassEvent(
override val id: GameId,
val player: Player,
) : GameEvent

View File

@@ -0,0 +1,12 @@
package eventDemo.app.event.event
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player
/**
* This [GameEvent] is sent when a player is ready.
*/
data class PlayerReadyEvent(
override val id: GameId,
val player: Player,
) : GameEvent

View File

@@ -1,6 +1,7 @@
package eventDemo.app
package eventDemo.app.eventListener
import eventDemo.app.event.GameEvent
import eventDemo.app.entity.GameId
import eventDemo.app.event.event.GameEvent
import eventDemo.libs.event.EventBus
import eventDemo.shared.toFrame
import io.ktor.websocket.Frame

View File

@@ -1,8 +1,9 @@
package eventDemo.app
package eventDemo.app.eventListener
import eventDemo.app.event.GameEvent
import eventDemo.app.event.GameStartedEvent
import eventDemo.app.entity.GameId
import eventDemo.app.event.buildStateFromEventStream
import eventDemo.app.event.event.GameEvent
import eventDemo.app.event.event.GameStartedEvent
import eventDemo.libs.event.EventBus
import eventDemo.libs.event.EventStream

View File

@@ -0,0 +1,56 @@
package eventDemo.app.query
import eventDemo.app.entity.GameId
import eventDemo.app.event.GameEventStream
import eventDemo.app.event.buildStateFromEventStream
import eventDemo.app.event.event.CardIsPlayedEvent
import eventDemo.libs.event.readLastOf
import eventDemo.shared.GameIdSerializer
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.resources.get
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import kotlinx.serialization.Serializable
@Serializable
@Resource("/game/{id}")
class Game(
@Serializable(with = GameIdSerializer::class)
val id: GameId,
) {
@Serializable
@Resource("card/last")
class Card(
val game: Game,
)
@Serializable
@Resource("state")
class State(
val game: Game,
)
}
/**
* API routes to read the game state.
*/
fun Route.readTheGameState(eventStream: GameEventStream) {
authenticate {
// Read the last played card on the game.
get<Game.Card> { body ->
eventStream
.readLastOf<CardIsPlayedEvent, _, _>(body.game.id)
?.let { call.respond(it.card) }
?: call.response.status(HttpStatusCode.BadRequest)
}
// 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,6 +1,6 @@
package eventDemo.configuration
import eventDemo.app.GameEventReactionListener
import eventDemo.app.eventListener.GameEventReactionListener
import io.ktor.server.application.Application
import org.koin.ktor.ext.get
@@ -11,11 +11,11 @@ fun Application.configure() {
configureSerialization()
configureSockets()
configureWebSocketsGameRoute(get(), get())
configureWebSockets()
declareWebSocketsGameRoute(get(), get())
configureHttp()
configureHttpRouting()
declareHttpGameRoute()
GameEventReactionListener(get(), get())
.init()

View File

@@ -5,9 +5,13 @@ import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.autohead.AutoHeadResponse
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.resources.Resources
import io.ktor.server.response.respondText
fun Application.configureHttp() {
fun Application.configureHttpRouting() {
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
@@ -18,6 +22,16 @@ fun Application.configureHttp() {
allowHeader("MyCustomHeader")
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
}
install(AutoHeadResponse)
install(Resources)
install(StatusPages) {
exception<BadRequestException> { call, cause ->
call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest)
}
exception<Throwable> { call, cause ->
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
}
}
}
class BadRequestException(

View File

@@ -0,0 +1,23 @@
package eventDemo.configuration
import eventDemo.shared.UUIDSerializer
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import java.util.UUID
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(
Json {
serializersModule =
SerializersModule {
contextual(UUID::class) { UUIDSerializer }
}
},
)
}
}

View File

@@ -0,0 +1,17 @@
package eventDemo.configuration
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.pingPeriod
import io.ktor.server.websocket.timeout
import java.time.Duration
fun Application.configureWebSockets() {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15)
timeout = Duration.ofSeconds(15)
maxFrameSize = Long.MAX_VALUE
masking = false
}
}

View File

@@ -0,0 +1,16 @@
package eventDemo.configuration
import eventDemo.app.command.gameSocket
import eventDemo.app.event.GameEventBus
import eventDemo.app.event.GameEventStream
import io.ktor.server.application.Application
import io.ktor.server.routing.routing
fun Application.declareWebSocketsGameRoute(
eventStream: GameEventStream,
eventBus: GameEventBus,
) {
routing {
gameSocket(eventStream, eventBus)
}
}

View File

@@ -0,0 +1,12 @@
package eventDemo.configuration
import eventDemo.app.query.readTheGameState
import io.ktor.server.application.Application
import io.ktor.server.routing.routing
import org.koin.ktor.ext.get
fun Application.declareHttpGameRoute() {
routing {
readTheGameState(get())
}
}

View File

@@ -1,30 +0,0 @@
package eventDemo.configuration
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
import io.ktor.server.plugins.autohead.AutoHeadResponse
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.resources.Resources
import io.ktor.server.response.respondText
import io.ktor.server.routing.routing
fun Application.configureHttpRouting() {
install(AutoHeadResponse)
install(Resources)
install(StatusPages) {
exception<BadRequestException> { call, cause ->
call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest)
}
exception<Throwable> { call, cause ->
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
}
}
routing {
readLastPlayedCard()
readGameState()
}
}

View File

@@ -1,50 +0,0 @@
package eventDemo.configuration
import eventDemo.app.GameEventPlayerNotificationListener
import eventDemo.app.actions.GameCommandHandler
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
import io.ktor.server.auth.authenticate
import io.ktor.server.auth.jwt.JWTPrincipal
import io.ktor.server.auth.principal
import io.ktor.server.routing.routing
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.pingPeriod
import io.ktor.server.websocket.timeout
import io.ktor.server.websocket.webSocket
import java.time.Duration
fun Application.configureSockets() {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15)
timeout = Duration.ofSeconds(15)
maxFrameSize = Long.MAX_VALUE
masking = false
}
}
fun Application.configureWebSocketsGameRoute(
eventStream: GameEventStream,
eventBus: GameEventBus,
) {
routing {
authenticate {
webSocket("/game") {
GameCommandHandler(eventStream, incoming, outgoing).init(call.getPlayer())
GameEventPlayerNotificationListener(eventBus, outgoing).init()
}
}
}
}
fun ApplicationCall.getPlayer() =
principal<JWTPrincipal>()!!.run {
Player(
id = payload.getClaim("playerid").asString(),
name = payload.getClaim("username").asString(),
)
}

View File

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

View File

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

View File

@@ -1,35 +1,16 @@
package eventDemo.configuration
package eventDemo.shared
import eventDemo.app.GameId
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player.PlayerId
import eventDemo.libs.command.CommandId
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import java.util.UUID
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(
Json {
serializersModule =
SerializersModule {
contextual(UUID::class) { UUIDSerializer }
}
},
)
}
}
object CommandIdSerializer : KSerializer<CommandId> {
override fun deserialize(decoder: Decoder): CommandId = CommandId(decoder.decodeString())

View File

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

View File

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