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.Card
import eventDemo.app.entity.Deck import eventDemo.app.entity.Deck
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player import eventDemo.app.entity.Player
import kotlinx.serialization.Serializable 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.GameState
import eventDemo.app.command.GameCommand import eventDemo.app.command.command.GameCommand
import eventDemo.app.command.GameCommandStream import eventDemo.app.command.command.IamReadyToPlayCommand
import eventDemo.app.command.PlayCardCommand import eventDemo.app.command.command.IwantToPlayCardCommand
import eventDemo.app.entity.Player import eventDemo.app.entity.Player
import eventDemo.app.event.CardIsPlayedEvent
import eventDemo.app.event.GameEvent
import eventDemo.app.event.GameEventStream import eventDemo.app.event.GameEventStream
import eventDemo.app.event.buildStateFromEventStream 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 io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -35,24 +36,22 @@ class GameCommandHandler(
*/ */
fun init(player: Player) { fun init(player: Player) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
commandStream.process { commandStream.process { command ->
if (it.payload.player.id != player.id) { if (command.payload.player.id != player.id) {
nack() 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( eventStream.publish(
CardIsPlayedEvent( CardIsPlayedEvent(
it.payload.gameId, command.payload.gameId,
it.payload.card, command.payload.card,
it.payload.player, command.payload.player,
), ),
) )
} else { } 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( canBePlayThisCard(
command.payload.player, command.payload.player,
command.payload.card, 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 package eventDemo.app.command
import eventDemo.app.command.command.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

@@ -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.Card
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player import eventDemo.app.entity.Player
import eventDemo.libs.command.Command
import eventDemo.libs.command.CommandId import eventDemo.libs.command.CommandId
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -13,15 +12,9 @@ import kotlinx.serialization.Serializable
*/ */
@Serializable @Serializable
@SerialName("PlayCard") @SerialName("PlayCard")
data class PlayCardCommand( data class IwantToPlayCardCommand(
override val payload: Payload, override val payload: Payload,
) : GameCommand { ) : GameCommand {
constructor(
gameId: GameId,
player: Player,
card: Card,
) : this(Payload(gameId, player, card))
override val name: String = "PlayCard" override val name: String = "PlayCard"
override val id: CommandId = CommandId() override val id: CommandId = CommandId()
@@ -32,14 +25,3 @@ data class PlayCardCommand(
val card: Card, val card: Card,
) : GameCommand.Payload ) : 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 package eventDemo.app.entity
import eventDemo.configuration.UUIDSerializer import eventDemo.shared.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,7 +1,7 @@
package eventDemo.app package eventDemo.app.entity
import eventDemo.configuration.GameIdSerializer
import eventDemo.libs.event.AggregateId import eventDemo.libs.event.AggregateId
import eventDemo.shared.GameIdSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import java.util.UUID

View File

@@ -1,8 +1,8 @@
package eventDemo.app.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.shared.PlayerIdSerializer
import eventDemo.shared.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
@@ -26,11 +26,3 @@ data class Player(
override fun toString(): String = id.toString() 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 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.EventBus
class GameEventBus( class GameEventBus(

View File

@@ -1,6 +1,7 @@
package eventDemo.app.event 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.EventBus
import eventDemo.libs.event.EventStream import eventDemo.libs.event.EventStream

View File

@@ -1,8 +1,15 @@
package eventDemo.app.event package eventDemo.app.event
import eventDemo.app.GameId
import eventDemo.app.GameState import eventDemo.app.GameState
import eventDemo.app.entity.Card 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 import eventDemo.libs.event.EventStream
fun GameId.buildStateFromEventStream(eventStream: EventStream<GameEvent, GameId>): GameState = 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.libs.event.EventBus
import eventDemo.shared.toFrame import eventDemo.shared.toFrame
import io.ktor.websocket.Frame 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.entity.GameId
import eventDemo.app.event.GameStartedEvent
import eventDemo.app.event.buildStateFromEventStream 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.EventBus
import eventDemo.libs.event.EventStream 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 package eventDemo.configuration
import eventDemo.app.GameEventReactionListener import eventDemo.app.eventListener.GameEventReactionListener
import io.ktor.server.application.Application import io.ktor.server.application.Application
import org.koin.ktor.ext.get import org.koin.ktor.ext.get
@@ -11,11 +11,11 @@ fun Application.configure() {
configureSerialization() configureSerialization()
configureSockets() configureWebSockets()
configureWebSocketsGameRoute(get(), get()) declareWebSocketsGameRoute(get(), get())
configureHttp()
configureHttpRouting() configureHttpRouting()
declareHttpGameRoute()
GameEventReactionListener(get(), get()) GameEventReactionListener(get(), get())
.init() .init()

View File

@@ -5,9 +5,13 @@ import io.ktor.http.HttpMethod
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
import io.ktor.server.plugins.autohead.AutoHeadResponse
import io.ktor.server.plugins.cors.routing.CORS 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) { install(CORS) {
allowMethod(HttpMethod.Options) allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put) allowMethod(HttpMethod.Put)
@@ -18,6 +22,16 @@ fun Application.configureHttp() {
allowHeader("MyCustomHeader") allowHeader("MyCustomHeader")
anyHost() // @TODO: Don't do this in production if possible. Try to limit it. 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( 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 package eventDemo.libs.command
import eventDemo.configuration.CommandIdSerializer import eventDemo.shared.CommandIdSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import java.util.UUID

View File

@@ -1,7 +1,7 @@
package eventDemo.shared package eventDemo.shared
import eventDemo.app.command.GameCommand import eventDemo.app.command.command.GameCommand
import eventDemo.app.event.GameEvent import eventDemo.app.event.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,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.app.entity.Player.PlayerId
import eventDemo.libs.command.CommandId 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.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import java.util.UUID import java.util.UUID
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(
Json {
serializersModule =
SerializersModule {
contextual(UUID::class) { UUIDSerializer }
}
},
)
}
}
object CommandIdSerializer : KSerializer<CommandId> { object CommandIdSerializer : KSerializer<CommandId> {
override fun deserialize(decoder: Decoder): CommandId = CommandId(decoder.decodeString()) 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.Card
import eventDemo.app.entity.GameId
import eventDemo.app.entity.Player import eventDemo.app.entity.Player
import eventDemo.app.event.CardIsPlayedEvent
import eventDemo.app.event.GameEventStream import eventDemo.app.event.GameEventStream
import eventDemo.app.event.event.CardIsPlayedEvent
import eventDemo.configuration.configure 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

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.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