add IamReadyToPlayCommand & refactoring
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
32
src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt
Normal file
32
src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt
Normal 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(),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
17
src/main/kotlin/eventDemo/app/command/command/GameCommand.kt
Normal file
17
src/main/kotlin/eventDemo/app/command/command/GameCommand.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
11
src/main/kotlin/eventDemo/app/entity/PlayerHand.kt
Normal file
11
src/main/kotlin/eventDemo/app/entity/PlayerHand.kt
Normal 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() }
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
13
src/main/kotlin/eventDemo/app/event/event/GameEvent.kt
Normal file
13
src/main/kotlin/eventDemo/app/event/event/GameEvent.kt
Normal 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
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
12
src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt
Normal file
12
src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
56
src/main/kotlin/eventDemo/app/query/ReadTheGameState.kt
Normal file
56
src/main/kotlin/eventDemo/app/query/ReadTheGameState.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
@@ -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 }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
12
src/main/kotlin/eventDemo/configuration/DeclareHttpRoutes.kt
Normal file
12
src/main/kotlin/eventDemo/configuration/DeclareHttpRoutes.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package eventDemo.libs.command
|
||||
|
||||
import eventDemo.configuration.CommandIdSerializer
|
||||
import eventDemo.shared.CommandIdSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user