From b4234a9b37a18da1a75eeb43f803f80fffd93b2b Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Fri, 14 Mar 2025 03:23:16 +0100 Subject: [PATCH] update ktlint rules --- .editorconfig | 2 + build.gradle.kts | 78 +++-- src/main/kotlin/eventDemo/Application.kt | 14 +- .../eventDemo/app/command/ErrorNotifier.kt | 34 +- .../app/command/GameCommandHandler.kt | 51 +-- .../eventDemo/app/command/GameCommandRoute.kt | 42 +-- .../app/command/GameCommandRunner.kt | 28 +- .../app/command/GameCommandStream.kt | 2 +- .../app/command/command/GameCommand.kt | 12 +- .../app/command/command/ICantPlayCommand.kt | 62 ++-- .../command/IWantToJoinTheGameCommand.kt | 46 +-- .../command/command/IWantToPlayCardCommand.kt | 68 ++-- .../command/command/IamReadyToPlayCommand.kt | 58 ++-- src/main/kotlin/eventDemo/app/entity/Card.kt | 170 +++++----- src/main/kotlin/eventDemo/app/entity/Deck.kt | 181 +++++----- .../kotlin/eventDemo/app/entity/GameId.kt | 5 +- .../kotlin/eventDemo/app/entity/Player.kt | 31 +- .../eventDemo/app/entity/PlayersHands.kt | 69 ++-- .../eventDemo/app/event/EventHandler.kt | 10 +- .../eventDemo/app/event/GameEventBus.kt | 2 +- .../eventDemo/app/event/GameEventHandler.kt | 42 +-- .../eventDemo/app/event/GameEventStore.kt | 2 +- .../eventDemo/app/event/GameEventStream.kt | 8 +- .../app/event/event/CardIsPlayedEvent.kt | 14 +- .../eventDemo/app/event/event/GameEvent.kt | 6 +- .../app/event/event/GameStartedEvent.kt | 52 +-- .../app/event/event/NewPlayerEvent.kt | 10 +- .../app/event/event/PlayerActionEvent.kt | 2 +- .../app/event/event/PlayerChoseColorEvent.kt | 14 +- .../app/event/event/PlayerHavePassEvent.kt | 14 +- .../app/event/event/PlayerReadyEvent.kt | 10 +- .../app/event/event/PlayerWinEvent.kt | 10 +- .../app/event/projection/GameState.kt | 307 ++++++++--------- .../app/event/projection/GameStateBuilder.kt | 194 +++++------ .../event/projection/GameStateRepository.kt | 56 +-- .../app/event/projection/Projection.kt | 4 +- .../ProjectionSnapshotRepositoryInMemory.kt | 321 +++++++++--------- .../PlayerNotificationEventListener.kt | 218 ++++++------ .../eventListener/ReactionEventListener.kt | 126 +++---- .../app/notification/ErrorNotification.kt | 6 +- .../notification/ItsTheTurnOfNotification.kt | 6 +- .../app/notification/Notification.kt | 4 +- .../PlayerAsJoinTheGameNotification.kt | 6 +- .../PlayerAsPlayACardNotification.kt | 8 +- .../PlayerHavePassNotification.kt | 6 +- .../PlayerWasChoseTheCardColorNotification.kt | 8 +- .../PlayerWasReadyNotification.kt | 6 +- .../app/notification/PlayerWinNotification.kt | 6 +- .../TheGameWasStartedNotification.kt | 6 +- .../WelcomeToTheGameNotification.kt | 6 +- .../notification/YourNewCardNotification.kt | 6 +- .../eventDemo/app/query/ReadTheGameState.kt | 56 +-- .../eventDemo/configuration/Configure.kt | 16 +- .../eventDemo/configuration/ConfigureAuth.kt | 64 ++-- .../eventDemo/configuration/ConfigureDI.kt | 46 +-- .../configuration/ConfigureGameListener.kt | 4 +- .../eventDemo/configuration/ConfigureHttp.kt | 54 +-- .../configuration/ConfigureSerialization.kt | 96 +++--- .../configuration/ConfigureWebSockets.kt | 12 +- .../ConfigureWebSocketsGameRoute.kt | 10 +- .../configuration/DeclareHttpRoutes.kt | 6 +- .../eventDemo/libs/FrameChannelConverter.kt | 36 +- .../kotlin/eventDemo/libs/command/Command.kt | 9 +- .../eventDemo/libs/command/CommandStream.kt | 36 +- .../libs/command/CommandStreamChannel.kt | 141 ++++---- src/main/kotlin/eventDemo/libs/event/Event.kt | 10 +- .../kotlin/eventDemo/libs/event/EventBus.kt | 16 +- .../eventDemo/libs/event/EventBusInMemory.kt | 32 +- .../kotlin/eventDemo/libs/event/EventStore.kt | 4 +- .../libs/event/EventStoreInMemory.kt | 8 +- .../eventDemo/libs/event/EventStream.kt | 16 +- .../libs/event/EventStreamInMemory.kt | 45 +-- .../eventDemo/libs/event/VersionBuilder.kt | 4 +- .../libs/event/VersionBuilderLocal.kt | 21 +- src/test/kotlin/eventDemo/Helpers.kt | 6 +- .../app/command/GameCommandHandlerTest.kt | 42 +-- .../app/command/GameCommandRunnerTest.kt | 6 +- .../command/command/ICantPlayCommandTest.kt | 6 +- .../command/IWantToJoinTheGameCommandTest.kt | 6 +- .../command/IWantToPlayCardCommandTest.kt | 6 +- .../command/IamReadyToPlayCommandTest.kt | 6 +- .../kotlin/eventDemo/app/entity/DeckTest.kt | 160 ++++----- .../eventDemo/app/entity/PlayerHandKtTest.kt | 58 ++-- .../eventDemo/app/entity/PlayersHandsTest.kt | 12 +- .../app/event/GameEventHandlerTest.kt | 10 +- .../event/projection/GameStateBuilderTest.kt | 214 ++++++------ .../projection/GameStateRepositoryTest.kt | 194 +++++------ .../app/event/projection/GameStateTest.kt | 20 +- ...rojectionSnapshotRepositoryInMemoryTest.kt | 234 ++++++------- .../eventDemo/app/query/GameStateRouteTest.kt | 152 ++++----- .../eventDemo/app/query/GameStateTest.kt | 240 ++++++------- .../eventDemo/app/query/TestHttpClient.kt | 12 +- .../libs/FrameChannelConverterTest.kt | 56 +-- .../libs/command/CommandStreamChannelTest.kt | 32 +- .../libs/event/EventBusInMemoryTest.kt | 8 +- .../libs/event/EventStreamInMemoryTest.kt | 14 +- .../libs/event/VersionBuilderLocalTest.kt | 66 ++-- 97 files changed, 2392 insertions(+), 2359 deletions(-) diff --git a/.editorconfig b/.editorconfig index 2017784..b8fefcc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,3 +4,5 @@ ktlint_standard = enabled ktlint_experimental = enabled ktlint_standard_string-template-indent = enabled ktlint_standard_multiline-expression-wrapping = enabled +ktlint_function_signature_body_expression_wrapping = always +indent_size = 2 diff --git a/build.gradle.kts b/build.gradle.kts index 48f8434..3fa714b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,5 @@ @file:Suppress("PropertyName") -@Suppress("ktlint:standard:property-naming") val ktor_version: String by project val kotlin_version: String by project val kotlin_serialization_version: String by project @@ -10,64 +9,63 @@ val kotlin_logging_version: String by project val kotest_version: String by project plugins { - kotlin("jvm") version "2.1.10" - id("io.ktor.plugin") version "2.3.13" - id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10" - id("org.jlleitschuh.gradle.ktlint") version "12.2.0" + kotlin("jvm") version "2.1.10" + id("io.ktor.plugin") version "2.3.13" + id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10" + id("org.jlleitschuh.gradle.ktlint") version "12.2.0" } group = "io.github.flecomte" version = "0.0.1" application { - mainClass.set("eventDemo.ApplicationKt") + mainClass.set("eventDemo.ApplicationKt") - val isDevelopment: Boolean = project.ext.has("development") - applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") } configure { - version.set("1.5.0") - enableExperimentalRules.set(true) + version.set("1.5.0") } repositories { - mavenCentral() + mavenCentral() } tasks.withType().configureEach { - useJUnitPlatform() + useJUnitPlatform() } dependencies { - implementation("io.ktor:ktor-server-core-jvm") - implementation("io.ktor:ktor-server-auth-jvm") - implementation("io.ktor:ktor-server-auth-jwt-jvm") - implementation("io.ktor:ktor-server-auto-head-response-jvm") - implementation("io.ktor:ktor-server-resources") - implementation("io.ktor:ktor-server-content-negotiation-jvm") - implementation("io.ktor:ktor-serialization-kotlinx-json-jvm") - implementation("io.ktor:ktor-server-websockets-jvm") - implementation("io.ktor:ktor-server-cors-jvm") - implementation("io.ktor:ktor-server-host-common-jvm") - implementation("io.ktor:ktor-server-status-pages-jvm") - implementation("io.ktor:ktor-server-netty-jvm") - implementation("io.ktor:ktor-server-data-conversion") - implementation("io.ktor:ktor-client-content-negotiation") - implementation("io.ktor:ktor-client-auth") - implementation("ch.qos.logback:logback-classic:$logback_version") - implementation("io.insert-koin:koin-ktor:$koin_version") - implementation("io.insert-koin:koin-logger-slf4j:$koin_version") - implementation("io.github.oshai:kotlin-logging-jvm:$kotlin_logging_version") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlin_serialization_version") - implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2") + implementation("io.ktor:ktor-server-core-jvm") + implementation("io.ktor:ktor-server-auth-jvm") + implementation("io.ktor:ktor-server-auth-jwt-jvm") + implementation("io.ktor:ktor-server-auto-head-response-jvm") + implementation("io.ktor:ktor-server-resources") + implementation("io.ktor:ktor-server-content-negotiation-jvm") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm") + implementation("io.ktor:ktor-server-websockets-jvm") + implementation("io.ktor:ktor-server-cors-jvm") + implementation("io.ktor:ktor-server-host-common-jvm") + implementation("io.ktor:ktor-server-status-pages-jvm") + implementation("io.ktor:ktor-server-netty-jvm") + implementation("io.ktor:ktor-server-data-conversion") + implementation("io.ktor:ktor-client-content-negotiation") + implementation("io.ktor:ktor-client-auth") + implementation("ch.qos.logback:logback-classic:$logback_version") + implementation("io.insert-koin:koin-ktor:$koin_version") + implementation("io.insert-koin:koin-logger-slf4j:$koin_version") + implementation("io.github.oshai:kotlin-logging-jvm:$kotlin_logging_version") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlin_serialization_version") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2") - // Force version of sub library (for security) - implementation("commons-codec:commons-codec:1.13") + // Force version of sub library (for security) + implementation("commons-codec:commons-codec:1.13") - testImplementation("io.kotest:kotest-extensions-koin:$kotest_version") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") - testImplementation("io.ktor:ktor-server-test-host-jvm:2.3.11") - testImplementation("io.kotest:kotest-runner-junit5:$kotest_version") - testImplementation("io.mockk:mockk:1.13.17") + testImplementation("io.kotest:kotest-extensions-koin:$kotest_version") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") + testImplementation("io.ktor:ktor-server-test-host-jvm:2.3.11") + testImplementation("io.kotest:kotest-runner-junit5:$kotest_version") + testImplementation("io.mockk:mockk:1.13.17") } diff --git a/src/main/kotlin/eventDemo/Application.kt b/src/main/kotlin/eventDemo/Application.kt index 03fac99..04093ff 100644 --- a/src/main/kotlin/eventDemo/Application.kt +++ b/src/main/kotlin/eventDemo/Application.kt @@ -6,11 +6,11 @@ import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty fun main() { - embeddedServer( - factory = Netty, - port = 8080, - host = "0.0.0.0", - module = Application::configure, - watchPaths = listOf("classes"), - ).start(wait = true) + embeddedServer( + factory = Netty, + port = 8080, + host = "0.0.0.0", + module = Application::configure, + watchPaths = listOf("classes"), + ).start(wait = true) } diff --git a/src/main/kotlin/eventDemo/app/command/ErrorNotifier.kt b/src/main/kotlin/eventDemo/app/command/ErrorNotifier.kt index a17b81f..4fcbea1 100644 --- a/src/main/kotlin/eventDemo/app/command/ErrorNotifier.kt +++ b/src/main/kotlin/eventDemo/app/command/ErrorNotifier.kt @@ -9,21 +9,21 @@ import kotlinx.coroutines.channels.SendChannel typealias ErrorNotifier = suspend (String) -> Unit fun errorNotifier( - command: GameCommand, - channel: SendChannel, + command: GameCommand, + channel: SendChannel, ): ErrorNotifier = - { - val logger = KotlinLogging.logger { } - ErrorNotification(message = it) - .let { notification -> - logger.atWarn { - message = "Notification ERROR sent: ${notification.message}" - payload = - mapOf( - "notification" to notification, - "command" to command, - ) - } - channel.send(notification) - } - } + { + val logger = KotlinLogging.logger { } + ErrorNotification(message = it) + .let { notification -> + logger.atWarn { + message = "Notification ERROR sent: ${notification.message}" + payload = + mapOf( + "notification" to notification, + "command" to command, + ) + } + channel.send(notification) + } + } diff --git a/src/main/kotlin/eventDemo/app/command/GameCommandHandler.kt b/src/main/kotlin/eventDemo/app/command/GameCommandHandler.kt index 5c56c7b..222596e 100644 --- a/src/main/kotlin/eventDemo/app/command/GameCommandHandler.kt +++ b/src/main/kotlin/eventDemo/app/command/GameCommandHandler.kt @@ -15,32 +15,33 @@ import kotlinx.coroutines.channels.SendChannel * This action can be executing an action and produce a new [GameEvent] after verification. */ class GameCommandHandler( - private val commandStreamChannel: CommandStreamChannelBuilder, - private val runner: GameCommandRunner, + private val commandStreamChannel: CommandStreamChannelBuilder, + private val runner: GameCommandRunner, ) { - private val logger = KotlinLogging.logger { } + private val logger = KotlinLogging.logger { } - /** - * Init the handler - */ - suspend fun handle( - player: Player, - incomingCommandChannel: ReceiveChannel, - outgoingErrorChannelNotification: SendChannel, - ) = commandStreamChannel(incomingCommandChannel) - .process { command -> - if (command.payload.player.id != player.id) { - logger.atWarn { - message = "Handle command Refuse, the player of the command is not the same: $command" - payload = mapOf("command" to command) - } - nack() - } else { - logger.atInfo { - message = "Handle command: $command" - payload = mapOf("command" to command) - } - runner.run(command, outgoingErrorChannelNotification) - } + /** + * Init the handler + */ + suspend fun handle( + player: Player, + incomingCommandChannel: ReceiveChannel, + outgoingErrorChannelNotification: SendChannel, + ) = + commandStreamChannel(incomingCommandChannel) + .process { command -> + if (command.payload.player.id != player.id) { + logger.atWarn { + message = "Handle command Refuse, the player of the command is not the same: $command" + payload = mapOf("command" to command) + } + nack() + } else { + logger.atInfo { + message = "Handle command: $command" + payload = mapOf("command" to command) + } + runner.run(command, outgoingErrorChannelNotification) } + } } diff --git a/src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt b/src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt index b46b6ff..12f8812 100644 --- a/src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt +++ b/src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt @@ -18,29 +18,29 @@ import kotlinx.coroutines.launch @DelicateCoroutinesApi fun Route.gameSocket( - playerNotificationListener: PlayerNotificationEventListener, - commandHandler: GameCommandHandler, + playerNotificationListener: PlayerNotificationEventListener, + commandHandler: GameCommandHandler, ) { - authenticate { - webSocket("/game") { - val currentPlayer = call.getPlayer() - val outgoingFrameChannel: SendChannel = fromFrameChannel(outgoing) - GlobalScope.launch { - commandHandler.handle( - currentPlayer, - toObjectChannel(incoming), - outgoingFrameChannel, - ) - } - playerNotificationListener.startListening(outgoingFrameChannel, currentPlayer) - } + authenticate { + webSocket("/game") { + val currentPlayer = call.getPlayer() + val outgoingFrameChannel: SendChannel = fromFrameChannel(outgoing) + GlobalScope.launch { + commandHandler.handle( + currentPlayer, + toObjectChannel(incoming), + outgoingFrameChannel, + ) + } + playerNotificationListener.startListening(outgoingFrameChannel, currentPlayer) } + } } private fun ApplicationCall.getPlayer() = - principal()!!.run { - Player( - id = payload.getClaim("playerid").asString(), - name = payload.getClaim("username").asString(), - ) - } + principal()!!.run { + Player( + id = payload.getClaim("playerid").asString(), + name = payload.getClaim("username").asString(), + ) + } diff --git a/src/main/kotlin/eventDemo/app/command/GameCommandRunner.kt b/src/main/kotlin/eventDemo/app/command/GameCommandRunner.kt index 602d874..088f408 100644 --- a/src/main/kotlin/eventDemo/app/command/GameCommandRunner.kt +++ b/src/main/kotlin/eventDemo/app/command/GameCommandRunner.kt @@ -11,21 +11,21 @@ import eventDemo.app.notification.Notification import kotlinx.coroutines.channels.SendChannel class GameCommandRunner( - private val eventHandler: GameEventHandler, - private val gameStateRepository: GameStateRepository, + private val eventHandler: GameEventHandler, + private val gameStateRepository: GameStateRepository, ) { - suspend fun run( - command: GameCommand, - outgoingErrorChannelNotification: SendChannel, - ) { - val gameState = gameStateRepository.getLast(command.payload.aggregateId) - val errorNotifier = errorNotifier(command, outgoingErrorChannelNotification) + suspend fun run( + command: GameCommand, + outgoingErrorChannelNotification: SendChannel, + ) { + val gameState = gameStateRepository.getLast(command.payload.aggregateId) + val errorNotifier = errorNotifier(command, outgoingErrorChannelNotification) - when (command) { - is IWantToPlayCardCommand -> command.run(gameState, errorNotifier, this.eventHandler) - is IamReadyToPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler) - is IWantToJoinTheGameCommand -> command.run(gameState, errorNotifier, this.eventHandler) - is ICantPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler) - } + when (command) { + is IWantToPlayCardCommand -> command.run(gameState, errorNotifier, this.eventHandler) + is IamReadyToPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler) + is IWantToJoinTheGameCommand -> command.run(gameState, errorNotifier, this.eventHandler) + is ICantPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler) } + } } diff --git a/src/main/kotlin/eventDemo/app/command/GameCommandStream.kt b/src/main/kotlin/eventDemo/app/command/GameCommandStream.kt index 89ee42a..60a11d9 100644 --- a/src/main/kotlin/eventDemo/app/command/GameCommandStream.kt +++ b/src/main/kotlin/eventDemo/app/command/GameCommandStream.kt @@ -9,5 +9,5 @@ import kotlinx.coroutines.channels.ReceiveChannel * A stream to publish and read the game command. */ class GameCommandStream( - incoming: ReceiveChannel, + incoming: ReceiveChannel, ) : CommandStream by CommandStreamChannel(incoming) diff --git a/src/main/kotlin/eventDemo/app/command/command/GameCommand.kt b/src/main/kotlin/eventDemo/app/command/command/GameCommand.kt index 788ecb3..1687895 100644 --- a/src/main/kotlin/eventDemo/app/command/command/GameCommand.kt +++ b/src/main/kotlin/eventDemo/app/command/command/GameCommand.kt @@ -7,11 +7,11 @@ import kotlinx.serialization.Serializable @Serializable sealed interface GameCommand : Command { - val payload: Payload + val payload: Payload - @Serializable - sealed interface Payload { - val aggregateId: GameId - val player: Player - } + @Serializable + sealed interface Payload { + val aggregateId: GameId + val player: Player + } } diff --git a/src/main/kotlin/eventDemo/app/command/command/ICantPlayCommand.kt b/src/main/kotlin/eventDemo/app/command/command/ICantPlayCommand.kt index 3053465..9ac466c 100644 --- a/src/main/kotlin/eventDemo/app/command/command/ICantPlayCommand.kt +++ b/src/main/kotlin/eventDemo/app/command/command/ICantPlayCommand.kt @@ -14,39 +14,39 @@ import kotlinx.serialization.Serializable */ @Serializable data class ICantPlayCommand( - override val payload: Payload, + override val payload: Payload, ) : GameCommand { - override val id: CommandId = CommandId() + override val id: CommandId = CommandId() - @Serializable - data class Payload( - override val aggregateId: GameId, - override val player: Player, - ) : GameCommand.Payload + @Serializable + data class Payload( + override val aggregateId: GameId, + override val player: Player, + ) : GameCommand.Payload - suspend fun run( - state: GameState, - playerErrorNotifier: ErrorNotifier, - eventHandler: GameEventHandler, - ) { - if (state.currentPlayerTurn != payload.player) { - playerErrorNotifier("Its not your turn!") - return - } - val playableCards = state.playableCards(payload.player) - if (playableCards.isEmpty()) { - val takenCard = state.deck.stack.first() - - eventHandler.handle(payload.aggregateId) { - PlayerHavePassEvent( - aggregateId = payload.aggregateId, - player = payload.player, - takenCard = takenCard, - version = it, - ) - } - } else { - playerErrorNotifier("You can and must play one card, like ${playableCards.first()::class.simpleName}") - } + suspend fun run( + state: GameState, + playerErrorNotifier: ErrorNotifier, + eventHandler: GameEventHandler, + ) { + if (state.currentPlayerTurn != payload.player) { + playerErrorNotifier("Its not your turn!") + return } + val playableCards = state.playableCards(payload.player) + if (playableCards.isEmpty()) { + val takenCard = state.deck.stack.first() + + eventHandler.handle(payload.aggregateId) { + PlayerHavePassEvent( + aggregateId = payload.aggregateId, + player = payload.player, + takenCard = takenCard, + version = it, + ) + } + } else { + playerErrorNotifier("You can and must play one card, like ${playableCards.first()::class.simpleName}") + } + } } diff --git a/src/main/kotlin/eventDemo/app/command/command/IWantToJoinTheGameCommand.kt b/src/main/kotlin/eventDemo/app/command/command/IWantToJoinTheGameCommand.kt index 3b8f25c..5bacb69 100644 --- a/src/main/kotlin/eventDemo/app/command/command/IWantToJoinTheGameCommand.kt +++ b/src/main/kotlin/eventDemo/app/command/command/IWantToJoinTheGameCommand.kt @@ -14,31 +14,31 @@ import kotlinx.serialization.Serializable */ @Serializable data class IWantToJoinTheGameCommand( - override val payload: Payload, + override val payload: Payload, ) : GameCommand { - override val id: CommandId = CommandId() + override val id: CommandId = CommandId() - @Serializable - data class Payload( - override val aggregateId: GameId, - override val player: Player, - ) : GameCommand.Payload + @Serializable + data class Payload( + override val aggregateId: GameId, + override val player: Player, + ) : GameCommand.Payload - suspend fun run( - state: GameState, - playerErrorNotifier: ErrorNotifier, - eventHandler: GameEventHandler, - ) { - if (!state.isStarted) { - eventHandler.handle(payload.aggregateId) { - NewPlayerEvent( - aggregateId = payload.aggregateId, - player = payload.player, - version = it, - ) - } - } else { - playerErrorNotifier("The game is already started") - } + suspend fun run( + state: GameState, + playerErrorNotifier: ErrorNotifier, + eventHandler: GameEventHandler, + ) { + if (!state.isStarted) { + eventHandler.handle(payload.aggregateId) { + NewPlayerEvent( + aggregateId = payload.aggregateId, + player = payload.player, + version = it, + ) + } + } else { + playerErrorNotifier("The game is already started") } + } } diff --git a/src/main/kotlin/eventDemo/app/command/command/IWantToPlayCardCommand.kt b/src/main/kotlin/eventDemo/app/command/command/IWantToPlayCardCommand.kt index af3aa6c..f39798a 100644 --- a/src/main/kotlin/eventDemo/app/command/command/IWantToPlayCardCommand.kt +++ b/src/main/kotlin/eventDemo/app/command/command/IWantToPlayCardCommand.kt @@ -15,42 +15,42 @@ import kotlinx.serialization.Serializable */ @Serializable data class IWantToPlayCardCommand( - override val payload: Payload, + override val payload: Payload, ) : GameCommand { - override val id: CommandId = CommandId() + override val id: CommandId = CommandId() - @Serializable - data class Payload( - override val aggregateId: GameId, - override val player: Player, - val card: Card, - ) : GameCommand.Payload + @Serializable + data class Payload( + override val aggregateId: GameId, + override val player: Player, + val card: Card, + ) : GameCommand.Payload - suspend fun run( - state: GameState, - playerErrorNotifier: ErrorNotifier, - eventHandler: GameEventHandler, - ) { - if (!state.isStarted) { - playerErrorNotifier("The game is Not started") - return - } - if (state.currentPlayerTurn != payload.player) { - playerErrorNotifier("Its not your turn!") - return - } - - if (state.canBePlayThisCard(payload.player, payload.card)) { - eventHandler.handle(payload.aggregateId) { - CardIsPlayedEvent( - aggregateId = payload.aggregateId, - card = payload.card, - player = payload.player, - version = it, - ) - } - } else { - playerErrorNotifier("You cannot play this card") - } + suspend fun run( + state: GameState, + playerErrorNotifier: ErrorNotifier, + eventHandler: GameEventHandler, + ) { + if (!state.isStarted) { + playerErrorNotifier("The game is Not started") + return } + if (state.currentPlayerTurn != payload.player) { + playerErrorNotifier("Its not your turn!") + return + } + + if (state.canBePlayThisCard(payload.player, payload.card)) { + eventHandler.handle(payload.aggregateId) { + CardIsPlayedEvent( + aggregateId = payload.aggregateId, + card = payload.card, + player = payload.player, + version = it, + ) + } + } else { + playerErrorNotifier("You cannot play this card") + } + } } diff --git a/src/main/kotlin/eventDemo/app/command/command/IamReadyToPlayCommand.kt b/src/main/kotlin/eventDemo/app/command/command/IamReadyToPlayCommand.kt index 30844ff..6c4d559 100644 --- a/src/main/kotlin/eventDemo/app/command/command/IamReadyToPlayCommand.kt +++ b/src/main/kotlin/eventDemo/app/command/command/IamReadyToPlayCommand.kt @@ -14,38 +14,38 @@ import kotlinx.serialization.Serializable */ @Serializable data class IamReadyToPlayCommand( - override val payload: Payload, + override val payload: Payload, ) : GameCommand { - override val id: CommandId = CommandId() + override val id: CommandId = CommandId() - @Serializable - data class Payload( - override val aggregateId: GameId, - override val player: Player, - ) : GameCommand.Payload + @Serializable + data class Payload( + override val aggregateId: GameId, + override val player: Player, + ) : GameCommand.Payload - suspend fun run( - state: GameState, - playerErrorNotifier: ErrorNotifier, - eventHandler: GameEventHandler, - ) { - val playerExist: Boolean = state.players.contains(payload.player) - val playerIsAlreadyReady: Boolean = state.readyPlayers.contains(payload.player) + suspend fun run( + state: GameState, + playerErrorNotifier: ErrorNotifier, + eventHandler: GameEventHandler, + ) { + val playerExist: Boolean = state.players.contains(payload.player) + val playerIsAlreadyReady: Boolean = state.readyPlayers.contains(payload.player) - if (state.isStarted) { - playerErrorNotifier("The game is already started") - } else if (!playerExist) { - playerErrorNotifier("You are not in the game") - } else if (playerIsAlreadyReady) { - playerErrorNotifier("You are already ready") - } else { - eventHandler.handle(payload.aggregateId) { - PlayerReadyEvent( - aggregateId = payload.aggregateId, - player = payload.player, - version = it, - ) - } - } + if (state.isStarted) { + playerErrorNotifier("The game is already started") + } else if (!playerExist) { + playerErrorNotifier("You are not in the game") + } else if (playerIsAlreadyReady) { + playerErrorNotifier("You are already ready") + } else { + eventHandler.handle(payload.aggregateId) { + PlayerReadyEvent( + aggregateId = payload.aggregateId, + player = payload.player, + version = it, + ) + } } + } } diff --git a/src/main/kotlin/eventDemo/app/entity/Card.kt b/src/main/kotlin/eventDemo/app/entity/Card.kt index 03238b8..e8f49f2 100644 --- a/src/main/kotlin/eventDemo/app/entity/Card.kt +++ b/src/main/kotlin/eventDemo/app/entity/Card.kt @@ -10,100 +10,100 @@ import java.util.UUID */ @Serializable sealed interface Card { - val id: UUID + val id: UUID - /** - * The color of a card - */ - @Serializable - enum class Color { - Blue, - Red, - Yellow, - Green, - } + /** + * The color of a card + */ + @Serializable + enum class Color { + Blue, + Red, + Yellow, + Green, + } - sealed interface ColorCard : Card { - val color: Color - } + sealed interface ColorCard : Card { + val color: Color + } - /** - * A play card with color and number - */ - @Serializable - @SerialName("Simple") - data class NumericCard( - val number: Int, - override val color: Color, - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - ) : Card, - ColorCard + /** + * A play card with color and number + */ + @Serializable + @SerialName("Simple") + data class NumericCard( + val number: Int, + override val color: Color, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + ) : Card, + ColorCard - sealed interface Special : Card + sealed interface Special : Card - /** - * A revert card to revert the order of the turn. - */ - @Serializable - @SerialName("Reverse") - data class ReverseCard( - override val color: Color, - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - ) : Special, - ColorCard + /** + * A revert card to revert the order of the turn. + */ + @Serializable + @SerialName("Reverse") + data class ReverseCard( + override val color: Color, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + ) : Special, + ColorCard - sealed interface PassTurnCard : Card + sealed interface PassTurnCard : Card - /** - * A pass card to pass the turn of the next player. - */ - @Serializable - @SerialName("Pass") - data class PassCard( - override val color: Color, - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - ) : Special, - ColorCard, - PassTurnCard + /** + * A pass card to pass the turn of the next player. + */ + @Serializable + @SerialName("Pass") + data class PassCard( + override val color: Color, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + ) : Special, + ColorCard, + PassTurnCard - /** - * A play card to force the next player to take 2 card and pass the turn. - */ - @Serializable - @SerialName("Plus2") - data class Plus2Card( - override val color: Color, - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - ) : Special, - ColorCard, - PassTurnCard + /** + * A play card to force the next player to take 2 card and pass the turn. + */ + @Serializable + @SerialName("Plus2") + data class Plus2Card( + override val color: Color, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + ) : Special, + ColorCard, + PassTurnCard - sealed interface AllColorCard : Card + sealed interface AllColorCard : Card - /** - * A play card to force the next player to take 4 card and pass the turn. - */ - @Serializable - @SerialName("Plus4") - class Plus4Card( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - ) : Special, - AllColorCard, - PassTurnCard + /** + * A play card to force the next player to take 4 card and pass the turn. + */ + @Serializable + @SerialName("Plus4") + class Plus4Card( + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + ) : Special, + AllColorCard, + PassTurnCard - /** - * A play card to change the color. - */ - @Serializable - @SerialName("ChangeColor") - class ChangeColorCard( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - ) : Special, - AllColorCard + /** + * A play card to change the color. + */ + @Serializable + @SerialName("ChangeColor") + class ChangeColorCard( + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + ) : Special, + AllColorCard } diff --git a/src/main/kotlin/eventDemo/app/entity/Deck.kt b/src/main/kotlin/eventDemo/app/entity/Deck.kt index b1a9e30..0ac55a2 100644 --- a/src/main/kotlin/eventDemo/app/entity/Deck.kt +++ b/src/main/kotlin/eventDemo/app/entity/Deck.kt @@ -4,119 +4,130 @@ import kotlinx.serialization.Serializable @Serializable data class Deck( - val stack: Stack = Stack(), - val discard: Discard = Discard(), - val playersHands: PlayersHands = PlayersHands(), + val stack: Stack = Stack(), + val discard: Discard = Discard(), + val playersHands: PlayersHands = PlayersHands(), ) { - constructor(players: Set) : - this(playersHands = PlayersHands(players)) + constructor(players: Set) : + this(playersHands = PlayersHands(players)) - fun shuffle(): Deck = copy(stack = stack.shuffle()) + fun shuffle(): Deck = + copy(stack = stack.shuffle()) - fun placeFirstCardOnDiscard(): Deck { - val takenCard = stack.first() - return copy( - stack = stack - takenCard, - discard = discard + takenCard, - ) + fun placeFirstCardOnDiscard(): Deck { + val takenCard = stack.first() + return copy( + stack = stack - takenCard, + discard = discard + takenCard, + ) + } + + fun takeOneCardFromStackTo(player: Player): Deck = + takeOne().let { (deck, newPlayerCard) -> + deck.copy( + playersHands = deck.playersHands.addCard(player, newPlayerCard), + ) } - fun takeOneCardFromStackTo(player: Player): Deck = - takeOne().let { (deck, newPlayerCard) -> - deck.copy( - playersHands = deck.playersHands.addCard(player, newPlayerCard), - ) - } + fun putOneCardFromHand( + player: Player, + card: Card, + ): Deck = + run { + // Validate parameters + val playerHand = + playersHands.getHand(player) + ?: error("No player on this game") + if (playerHand.none { it == card }) { + error("No card exist on the player hand") + } + }.let { + copy( + discard = discard + card, + playersHands = playersHands.removeCard(player, card), + ) + } - fun putOneCardFromHand( - player: Player, - card: Card, - ): Deck = - run { - // Validate parameters - val playerHand = - playersHands.getHand(player) - ?: error("No player on this game") - if (playerHand.none { it == card }) { - error("No card exist on the player hand") - } + fun playerHasNoCardLeft(): List = + playersHands + .filter { (playerId, hand) -> hand.isEmpty() } + .map { (playerId, hand) -> playerId } + + private fun take(n: Int): Pair> { + val takenCards = stack.take(n) + val newStack = stack.filterNot { takenCards.contains(it) }.toStack() + return Pair(copy(stack = newStack), takenCards) + } + + private fun takeOne(): Pair = + take(1).let { (deck, cards) -> Pair(deck, cards.first()) } + + companion object { + fun newWithoutPlayers(): Deck = + listOf(Card.Color.Red, Card.Color.Blue, Card.Color.Yellow, Card.Color.Green) + .flatMap { color -> + ((0..9) + (1..9)).map { Card.NumericCard(it, color) } + + (1..2).map { Card.Plus2Card(color) } + + (1..2).map { Card.ReverseCard(color) } + + (1..2).map { Card.PassCard(color) } }.let { - copy( - discard = discard + card, - playersHands = playersHands.removeCard(player, card), - ) - } - - fun playerHasNoCardLeft(): List = - playersHands - .filter { (playerId, hand) -> hand.isEmpty() } - .map { (playerId, hand) -> playerId } - - private fun take(n: Int): Pair> { - val takenCards = stack.take(n) - val newStack = stack.filterNot { takenCards.contains(it) }.toStack() - return Pair(copy(stack = newStack), takenCards) - } - - private fun takeOne(): Pair = take(1).let { (deck, cards) -> Pair(deck, cards.first()) } - - companion object { - fun newWithoutPlayers(): Deck = - listOf(Card.Color.Red, Card.Color.Blue, Card.Color.Yellow, Card.Color.Green) - .flatMap { color -> - ((0..9) + (1..9)).map { Card.NumericCard(it, color) } + - (1..2).map { Card.Plus2Card(color) } + - (1..2).map { Card.ReverseCard(color) } + - (1..2).map { Card.PassCard(color) } - }.let { - it + (1..4).map { Card.Plus4Card() } - }.toStack() - .let { Deck(it) } - } + it + (1..4).map { Card.Plus4Card() } + }.toStack() + .let { Deck(it) } + } } fun Deck.initHands( - players: Set, - handSize: Int = 7, + players: Set, + handSize: Int = 7, ): Deck { - // Copy cards from stack to the player hands - val deckWithEmptyHands = copy(playersHands = PlayersHands(players)) - return players.fold(deckWithEmptyHands) { acc: Deck, player: Player -> - val hand = acc.stack.take(handSize) - val newStack = acc.stack.filterNot { card: Card -> hand.contains(card) }.toStack() - copy( - stack = newStack, - playersHands = acc.playersHands.addCards(player, hand), - ) - } + // Copy cards from stack to the player hands + val deckWithEmptyHands = copy(playersHands = PlayersHands(players)) + return players.fold(deckWithEmptyHands) { acc: Deck, player: Player -> + val hand = acc.stack.take(handSize) + val newStack = acc.stack.filterNot { card: Card -> hand.contains(card) }.toStack() + copy( + stack = newStack, + playersHands = acc.playersHands.addCards(player, hand), + ) + } } @JvmInline @Serializable value class Stack( - private val cards: Set = emptySet(), + private val cards: Set = emptySet(), ) : Set by cards { - operator fun plus(card: Card): Stack = cards.plus(card).toStack() + operator fun plus(card: Card): Stack = + cards.plus(card).toStack() - operator fun minus(card: Card): Stack = cards.minus(card).toStack() + operator fun minus(card: Card): Stack = + cards.minus(card).toStack() - fun shuffle(): Stack = shuffled().toStack() + fun shuffle(): Stack = + shuffled().toStack() } -fun List.toStack(): Stack = Stack(this.toSet()) +fun List.toStack(): Stack = + Stack(this.toSet()) -fun Set.toStack(): Stack = Stack(this) +fun Set.toStack(): Stack = + Stack(this) @JvmInline @Serializable value class Discard( - private val cards: Set = emptySet(), + private val cards: Set = emptySet(), ) : Set by cards { - operator fun plus(card: Card): Discard = cards.plus(card).toDiscard() + operator fun plus(card: Card): Discard = + cards.plus(card).toDiscard() - operator fun minus(card: Card): Discard = cards.minus(card).toDiscard() + operator fun minus(card: Card): Discard = + cards.minus(card).toDiscard() } -fun List.toDiscard(): Discard = Discard(this.toSet()) +fun List.toDiscard(): Discard = + Discard(this.toSet()) -fun Set.toDiscard(): Discard = Discard(this) +fun Set.toDiscard(): Discard = + Discard(this) diff --git a/src/main/kotlin/eventDemo/app/entity/GameId.kt b/src/main/kotlin/eventDemo/app/entity/GameId.kt index 8207825..dd1c11c 100644 --- a/src/main/kotlin/eventDemo/app/entity/GameId.kt +++ b/src/main/kotlin/eventDemo/app/entity/GameId.kt @@ -11,7 +11,8 @@ import java.util.UUID @JvmInline @Serializable(with = GameIdSerializer::class) value class GameId( - override val id: UUID = UUID.randomUUID(), + override val id: UUID = UUID.randomUUID(), ) : AggregateId { - override fun toString(): String = id.toString() + override fun toString(): String = + id.toString() } diff --git a/src/main/kotlin/eventDemo/app/entity/Player.kt b/src/main/kotlin/eventDemo/app/entity/Player.kt index 43b13c6..76f3cfe 100644 --- a/src/main/kotlin/eventDemo/app/entity/Player.kt +++ b/src/main/kotlin/eventDemo/app/entity/Player.kt @@ -8,21 +8,22 @@ import java.util.UUID @Serializable data class Player( - val name: String, - @Serializable(with = PlayerIdSerializer::class) - val id: PlayerId = PlayerId(UUID.randomUUID()), + val name: String, + @Serializable(with = PlayerIdSerializer::class) + val id: PlayerId = PlayerId(UUID.randomUUID()), ) { - constructor(id: String, name: String) : this( - name, - PlayerId(UUID.fromString(id)), - ) + constructor(id: String, name: String) : this( + name, + PlayerId(UUID.fromString(id)), + ) - @Serializable - @JvmInline - value class PlayerId( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - ) : AggregateId { - override fun toString(): String = id.toString() - } + @Serializable + @JvmInline + value class PlayerId( + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + ) : AggregateId { + override fun toString(): String = + id.toString() + } } diff --git a/src/main/kotlin/eventDemo/app/entity/PlayersHands.kt b/src/main/kotlin/eventDemo/app/entity/PlayersHands.kt index ee70a24..96afccc 100644 --- a/src/main/kotlin/eventDemo/app/entity/PlayersHands.kt +++ b/src/main/kotlin/eventDemo/app/entity/PlayersHands.kt @@ -5,43 +5,46 @@ import kotlinx.serialization.Serializable @Serializable @JvmInline value class PlayersHands( - private val map: Map> = emptyMap(), + private val map: Map> = emptyMap(), ) : Map> by map { - constructor(players: Set) : - this(players.map { it.id }.associateWith { emptyList() }.toPlayersHands()) + constructor(players: Set) : + this(players.map { it.id }.associateWith { emptyList() }.toPlayersHands()) - fun getHand(player: Player): List? = this[player.id] + fun getHand(player: Player): List? = + this[player.id] - fun removeCard( - player: Player, - card: Card, - ): PlayersHands = - mapValues { (playerId, cards) -> - if (playerId == player.id) { - if (!cards.contains(card)) error("The hand no contain the card") - cards - card - } else { - cards - } - }.toPlayersHands() + fun removeCard( + player: Player, + card: Card, + ): PlayersHands = + mapValues { (playerId, cards) -> + if (playerId == player.id) { + if (!cards.contains(card)) error("The hand no contain the card") + cards - card + } else { + cards + } + }.toPlayersHands() - fun addCard( - player: Player, - newCard: Card, - ): PlayersHands = addCards(player, listOf(newCard)) + fun addCard( + player: Player, + newCard: Card, + ): PlayersHands = + addCards(player, listOf(newCard)) - fun addCards( - player: Player, - newCards: List, - ): PlayersHands = - mapValues { (p, cards) -> - if (p == player.id) { - if (cards.intersect(newCards).isNotEmpty()) error("The hand already contain the card") - cards + newCards - } else { - cards - } - }.toPlayersHands() + fun addCards( + player: Player, + newCards: List, + ): PlayersHands = + mapValues { (p, cards) -> + if (p == player.id) { + if (cards.intersect(newCards).isNotEmpty()) error("The hand already contain the card") + cards + newCards + } else { + cards + } + }.toPlayersHands() } -fun Map>.toPlayersHands(): PlayersHands = PlayersHands(this) +fun Map>.toPlayersHands(): PlayersHands = + PlayersHands(this) diff --git a/src/main/kotlin/eventDemo/app/event/EventHandler.kt b/src/main/kotlin/eventDemo/app/event/EventHandler.kt index 267689f..458f306 100644 --- a/src/main/kotlin/eventDemo/app/event/EventHandler.kt +++ b/src/main/kotlin/eventDemo/app/event/EventHandler.kt @@ -7,10 +7,10 @@ import eventDemo.libs.event.Event * A stream to publish and read the played card event. */ interface EventHandler, ID : AggregateId> { - fun registerProjectionBuilder(builder: (E) -> Unit) + fun registerProjectionBuilder(builder: (E) -> Unit) - fun handle( - aggregateId: ID, - buildEvent: (version: Int) -> E, - ): E + fun handle( + aggregateId: ID, + buildEvent: (version: Int) -> E, + ): E } diff --git a/src/main/kotlin/eventDemo/app/event/GameEventBus.kt b/src/main/kotlin/eventDemo/app/event/GameEventBus.kt index 2dbb9d4..ea4e79d 100644 --- a/src/main/kotlin/eventDemo/app/event/GameEventBus.kt +++ b/src/main/kotlin/eventDemo/app/event/GameEventBus.kt @@ -5,5 +5,5 @@ import eventDemo.app.event.event.GameEvent import eventDemo.libs.event.EventBus class GameEventBus( - bus: EventBus, + bus: EventBus, ) : EventBus by bus diff --git a/src/main/kotlin/eventDemo/app/event/GameEventHandler.kt b/src/main/kotlin/eventDemo/app/event/GameEventHandler.kt index c7bffdb..f39a5b0 100644 --- a/src/main/kotlin/eventDemo/app/event/GameEventHandler.kt +++ b/src/main/kotlin/eventDemo/app/event/GameEventHandler.kt @@ -12,30 +12,30 @@ import kotlin.concurrent.withLock * A stream to publish and read the played card event. */ class GameEventHandler( - private val eventBus: GameEventBus, - private val eventStore: GameEventStore, - private val versionBuilder: VersionBuilder, + private val eventBus: GameEventBus, + private val eventStore: GameEventStore, + private val versionBuilder: VersionBuilder, ) : EventHandler { - private val projectionsBuilders: ConcurrentLinkedQueue<(GameEvent) -> Unit> = ConcurrentLinkedQueue() - private val locks: ConcurrentHashMap = ConcurrentHashMap() + private val projectionsBuilders: ConcurrentLinkedQueue<(GameEvent) -> Unit> = ConcurrentLinkedQueue() + private val locks: ConcurrentHashMap = ConcurrentHashMap() - override fun registerProjectionBuilder(builder: GameProjectionBuilder) { - projectionsBuilders.add(builder) - } + override fun registerProjectionBuilder(builder: GameProjectionBuilder) { + projectionsBuilders.add(builder) + } - override fun handle( - aggregateId: GameId, - buildEvent: (version: Int) -> GameEvent, - ): GameEvent = - locks - .computeIfAbsent(aggregateId) { ReentrantLock() } - .withLock { - buildEvent(versionBuilder.buildNextVersion(aggregateId)) - .also { eventStore.publish(it) } - }.also { event -> - projectionsBuilders.forEach { it(event) } - eventBus.publish(event) - } + override fun handle( + aggregateId: GameId, + buildEvent: (version: Int) -> GameEvent, + ): GameEvent = + locks + .computeIfAbsent(aggregateId) { ReentrantLock() } + .withLock { + buildEvent(versionBuilder.buildNextVersion(aggregateId)) + .also { eventStore.publish(it) } + }.also { event -> + projectionsBuilders.forEach { it(event) } + eventBus.publish(event) + } } typealias GameProjectionBuilder = (GameEvent) -> Unit diff --git a/src/main/kotlin/eventDemo/app/event/GameEventStore.kt b/src/main/kotlin/eventDemo/app/event/GameEventStore.kt index 17bc5a0..61e8bb3 100644 --- a/src/main/kotlin/eventDemo/app/event/GameEventStore.kt +++ b/src/main/kotlin/eventDemo/app/event/GameEventStore.kt @@ -8,5 +8,5 @@ import eventDemo.libs.event.EventStore * A stream to publish and read the played card event. */ class GameEventStore( - private val eventStore: EventStore, + private val eventStore: EventStore, ) : EventStore by eventStore diff --git a/src/main/kotlin/eventDemo/app/event/GameEventStream.kt b/src/main/kotlin/eventDemo/app/event/GameEventStream.kt index bfeefd7..5629935 100644 --- a/src/main/kotlin/eventDemo/app/event/GameEventStream.kt +++ b/src/main/kotlin/eventDemo/app/event/GameEventStream.kt @@ -7,9 +7,9 @@ import eventDemo.libs.event.EventStream * A stream to publish and read the played card event. */ class GameEventStream( - private val eventStream: EventStream, + private val eventStream: EventStream, ) : EventStream by eventStream { - override fun publish(event: GameEvent) { - eventStream.publish(event) - } + override fun publish(event: GameEvent) { + eventStream.publish(event) + } } diff --git a/src/main/kotlin/eventDemo/app/event/event/CardIsPlayedEvent.kt b/src/main/kotlin/eventDemo/app/event/event/CardIsPlayedEvent.kt index 1c38d6d..827adcf 100644 --- a/src/main/kotlin/eventDemo/app/event/event/CardIsPlayedEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/CardIsPlayedEvent.kt @@ -11,12 +11,12 @@ import java.util.UUID * An [GameEvent] to represent a played card. */ data class CardIsPlayedEvent( - override val aggregateId: GameId, - val card: Card, - override val player: Player, - override val version: Int, + override val aggregateId: GameId, + val card: Card, + override val player: Player, + override val version: Int, ) : GameEvent, - PlayerActionEvent { - override val eventId: UUID = UUID.randomUUID() - override val createdAt: Instant = Clock.System.now() + PlayerActionEvent { + override val eventId: UUID = UUID.randomUUID() + override val createdAt: Instant = Clock.System.now() } diff --git a/src/main/kotlin/eventDemo/app/event/event/GameEvent.kt b/src/main/kotlin/eventDemo/app/event/event/GameEvent.kt index 8c5203f..89cebe2 100644 --- a/src/main/kotlin/eventDemo/app/event/event/GameEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/GameEvent.kt @@ -10,7 +10,7 @@ import java.util.UUID */ @Serializable sealed interface GameEvent : Event { - override val eventId: UUID - override val aggregateId: GameId - override val version: Int + override val eventId: UUID + override val aggregateId: GameId + override val version: Int } diff --git a/src/main/kotlin/eventDemo/app/event/event/GameStartedEvent.kt b/src/main/kotlin/eventDemo/app/event/event/GameStartedEvent.kt index 48565e3..c32e382 100644 --- a/src/main/kotlin/eventDemo/app/event/event/GameStartedEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/GameStartedEvent.kt @@ -12,37 +12,37 @@ import java.util.UUID * This [GameEvent] is sent when all players are ready. */ data class GameStartedEvent( - override val aggregateId: GameId, - val firstPlayer: Player, - val deck: Deck, - override val version: Int, + override val aggregateId: GameId, + val firstPlayer: Player, + val deck: Deck, + override val version: Int, ) : GameEvent { - override val eventId: UUID = UUID.randomUUID() - override val createdAt: Instant = Clock.System.now() + override val eventId: UUID = UUID.randomUUID() + override val createdAt: Instant = Clock.System.now() - companion object { - fun new( - id: GameId, - players: Set, - shuffleIsDisabled: Boolean = isDisabled, - version: Int, - ): GameStartedEvent = - GameStartedEvent( - aggregateId = id, - firstPlayer = if (shuffleIsDisabled) players.first() else players.random(), - deck = - Deck - .newWithoutPlayers() - .let { if (shuffleIsDisabled) it else it.shuffle() } - .initHands(players) - .placeFirstCardOnDiscard(), - version = version, - ) - } + companion object { + fun new( + id: GameId, + players: Set, + shuffleIsDisabled: Boolean = isDisabled, + version: Int, + ): GameStartedEvent = + GameStartedEvent( + aggregateId = id, + firstPlayer = if (shuffleIsDisabled) players.first() else players.random(), + deck = + Deck + .newWithoutPlayers() + .let { if (shuffleIsDisabled) it else it.shuffle() } + .initHands(players) + .placeFirstCardOnDiscard(), + version = version, + ) + } } private var isDisabled = false internal fun disableShuffleDeck() { - isDisabled = true + isDisabled = true } diff --git a/src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt b/src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt index 0d314f8..437e38f 100644 --- a/src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt @@ -10,10 +10,10 @@ import java.util.UUID * An [GameEvent] to represent a new player joining the game. */ data class NewPlayerEvent( - override val aggregateId: GameId, - val player: Player, - override val version: Int, + override val aggregateId: GameId, + val player: Player, + override val version: Int, ) : GameEvent { - override val eventId: UUID = UUID.randomUUID() - override val createdAt: Instant = Clock.System.now() + override val eventId: UUID = UUID.randomUUID() + override val createdAt: Instant = Clock.System.now() } diff --git a/src/main/kotlin/eventDemo/app/event/event/PlayerActionEvent.kt b/src/main/kotlin/eventDemo/app/event/event/PlayerActionEvent.kt index ffe9e41..f1f4fd5 100644 --- a/src/main/kotlin/eventDemo/app/event/event/PlayerActionEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/PlayerActionEvent.kt @@ -3,5 +3,5 @@ package eventDemo.app.event.event import eventDemo.app.entity.Player sealed interface PlayerActionEvent : GameEvent { - val player: Player + val player: Player } diff --git a/src/main/kotlin/eventDemo/app/event/event/PlayerChoseColorEvent.kt b/src/main/kotlin/eventDemo/app/event/event/PlayerChoseColorEvent.kt index 9adff0f..d2f5376 100644 --- a/src/main/kotlin/eventDemo/app/event/event/PlayerChoseColorEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/PlayerChoseColorEvent.kt @@ -11,12 +11,12 @@ import java.util.UUID * This [GameEvent] is sent when a player chose a color. */ data class PlayerChoseColorEvent( - override val aggregateId: GameId, - override val player: Player, - val color: Card.Color, - override val version: Int, + override val aggregateId: GameId, + override val player: Player, + val color: Card.Color, + override val version: Int, ) : GameEvent, - PlayerActionEvent { - override val eventId: UUID = UUID.randomUUID() - override val createdAt: Instant = Clock.System.now() + PlayerActionEvent { + override val eventId: UUID = UUID.randomUUID() + override val createdAt: Instant = Clock.System.now() } diff --git a/src/main/kotlin/eventDemo/app/event/event/PlayerHavePassEvent.kt b/src/main/kotlin/eventDemo/app/event/event/PlayerHavePassEvent.kt index 14d30fc..54bbbf3 100644 --- a/src/main/kotlin/eventDemo/app/event/event/PlayerHavePassEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/PlayerHavePassEvent.kt @@ -11,12 +11,12 @@ import java.util.UUID * This [GameEvent] is sent when a player can play. */ data class PlayerHavePassEvent( - override val aggregateId: GameId, - override val player: Player, - val takenCard: Card, - override val version: Int, + override val aggregateId: GameId, + override val player: Player, + val takenCard: Card, + override val version: Int, ) : GameEvent, - PlayerActionEvent { - override val eventId: UUID = UUID.randomUUID() - override val createdAt: Instant = Clock.System.now() + PlayerActionEvent { + override val eventId: UUID = UUID.randomUUID() + override val createdAt: Instant = Clock.System.now() } diff --git a/src/main/kotlin/eventDemo/app/event/event/PlayerReadyEvent.kt b/src/main/kotlin/eventDemo/app/event/event/PlayerReadyEvent.kt index f912bb9..da2fd24 100644 --- a/src/main/kotlin/eventDemo/app/event/event/PlayerReadyEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/PlayerReadyEvent.kt @@ -10,10 +10,10 @@ import java.util.UUID * This [GameEvent] is sent when a player is ready. */ data class PlayerReadyEvent( - override val aggregateId: GameId, - val player: Player, - override val version: Int, + override val aggregateId: GameId, + val player: Player, + override val version: Int, ) : GameEvent { - override val eventId: UUID = UUID.randomUUID() - override val createdAt: Instant = Clock.System.now() + override val eventId: UUID = UUID.randomUUID() + override val createdAt: Instant = Clock.System.now() } diff --git a/src/main/kotlin/eventDemo/app/event/event/PlayerWinEvent.kt b/src/main/kotlin/eventDemo/app/event/event/PlayerWinEvent.kt index 10aa237..b233d39 100644 --- a/src/main/kotlin/eventDemo/app/event/event/PlayerWinEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/PlayerWinEvent.kt @@ -10,10 +10,10 @@ import java.util.UUID * This [GameEvent] is sent when a player is ready. */ data class PlayerWinEvent( - override val aggregateId: GameId, - val player: Player, - override val version: Int, + override val aggregateId: GameId, + val player: Player, + override val version: Int, ) : GameEvent { - override val eventId: UUID = UUID.randomUUID() - override val createdAt: Instant = Clock.System.now() + override val eventId: UUID = UUID.randomUUID() + override val createdAt: Instant = Clock.System.now() } diff --git a/src/main/kotlin/eventDemo/app/event/projection/GameState.kt b/src/main/kotlin/eventDemo/app/event/projection/GameState.kt index d3de8e1..108d7cb 100644 --- a/src/main/kotlin/eventDemo/app/event/projection/GameState.kt +++ b/src/main/kotlin/eventDemo/app/event/projection/GameState.kt @@ -8,173 +8,174 @@ import kotlinx.serialization.Serializable @Serializable data class GameState( - override val aggregateId: GameId, - override val lastEventVersion: Int = 0, - val players: Set = emptySet(), - val currentPlayerTurn: Player? = null, - val cardOnCurrentStack: LastCard? = null, - val colorOnCurrentStack: Card.Color? = null, - val direction: Direction = Direction.CLOCKWISE, - val readyPlayers: Set = emptySet(), - val deck: Deck = Deck(players), - val isStarted: Boolean = false, - val playerWins: Set = emptySet(), + override val aggregateId: GameId, + override val lastEventVersion: Int = 0, + val players: Set = emptySet(), + val currentPlayerTurn: Player? = null, + val cardOnCurrentStack: LastCard? = null, + val colorOnCurrentStack: Card.Color? = null, + val direction: Direction = Direction.CLOCKWISE, + val readyPlayers: Set = emptySet(), + val deck: Deck = Deck(players), + val isStarted: Boolean = false, + val playerWins: Set = emptySet(), ) : Projection { - @Serializable - data class LastCard( - val card: Card, - val player: Player, - ) + @Serializable + data class LastCard( + val card: Card, + val player: Player, + ) - enum class Direction { - CLOCKWISE, - COUNTER_CLOCKWISE, - ; + enum class Direction { + CLOCKWISE, + COUNTER_CLOCKWISE, + ; - fun revert(): Direction = - if (this === CLOCKWISE) { - COUNTER_CLOCKWISE - } else { - CLOCKWISE - } + fun revert(): Direction = + if (this === CLOCKWISE) { + COUNTER_CLOCKWISE + } else { + CLOCKWISE + } + } + + val isReady: Boolean get() { + return players.size == readyPlayers.size && players.all { readyPlayers.contains(it) } + } + + private val currentPlayerIndex: Int? get() { + val i = players.indexOf(currentPlayerTurn) + return if (i == -1) { + null + } else { + i + } + } + + private fun nextPlayerIndex(direction: Direction): Int { + if (players.isEmpty()) return 0 + + return if (direction == Direction.CLOCKWISE) { + sidePlayerIndexClockwise + } else { + sidePlayerIndexCounterClockwise + } + } + + fun nextPlayer(direction: Direction): Player = + players.elementAt(nextPlayerIndex(direction)) + + private val sidePlayerIndexClockwise: Int by lazy { + if (players.isEmpty()) { + 0 + } else { + ((currentPlayerIndex ?: 0) + 1) % players.size + } + } + private val sidePlayerIndexCounterClockwise: Int by lazy { + if (players.isEmpty()) { + 0 + } else { + ((currentPlayerIndex ?: 0) - 1) % players.size + } + } + + val nextPlayerTurn: Player? by lazy { + if (players.isEmpty()) { + null + } else { + nextPlayer(direction) + } + } + + private val Player.currentIndex: Int get() = players.indexOf(this) + + fun Player.playerDiffIndex(nextPlayer: Player): Int = + if (direction == Direction.CLOCKWISE) { + nextPlayer.currentIndex + this.currentIndex + } else { + nextPlayer.currentIndex - this.currentIndex + }.let { it % players.size } + + val Player.cardOnBoardIsForYou: Boolean get() { + if (cardOnCurrentStack == null) error("No card") + return this.playerDiffIndex(cardOnCurrentStack.player) == 1 + } + + fun playableCards(player: Player): List = + deck + .playersHands + .getHand(player) + ?.filter { canBePlayThisCard(player, it) } + ?: emptyList() + + fun playerHasNoCardLeft(): List = + deck.playerHasNoCardLeft().map { playerId -> + players.find { it.id == playerId } ?: error("inconsistency detected between players") } - val isReady: Boolean get() { - return players.size == readyPlayers.size && players.all { readyPlayers.contains(it) } - } + fun canBePlayThisCard( + player: Player, + card: Card, + ): Boolean { + val cardOnBoard = cardOnCurrentStack?.card ?: return false + return when (cardOnBoard) { + is Card.NumericCard -> { + when (card) { + is Card.AllColorCard -> true + is Card.NumericCard -> card.number == cardOnBoard.number || card.color == cardOnBoard.color + is Card.ColorCard -> card.color == cardOnBoard.color + } + } - private val currentPlayerIndex: Int? get() { - val i = players.indexOf(currentPlayerTurn) - return if (i == -1) { - null + is Card.ReverseCard -> { + when (card) { + is Card.ReverseCard -> true + is Card.AllColorCard -> true + is Card.ColorCard -> card.color == cardOnBoard.color + } + } + + is Card.PassCard -> { + if (player.cardOnBoardIsForYou) { + false } else { - i + when (card) { + is Card.AllColorCard -> true + is Card.ColorCard -> card.color == cardOnBoard.color + } } - } + } - private fun nextPlayerIndex(direction: Direction): Int { - if (players.isEmpty()) return 0 + is Card.ChangeColorCard -> { + when (card) { + is Card.AllColorCard -> true + is Card.ColorCard -> card.color == colorOnCurrentStack + } + } - return if (direction == Direction.CLOCKWISE) { - sidePlayerIndexClockwise + is Card.Plus2Card -> { + if (player.cardOnBoardIsForYou && card is Card.Plus2Card) { + true } else { - sidePlayerIndexCounterClockwise + when (card) { + is Card.AllColorCard -> true + is Card.Plus2Card -> true + is Card.ColorCard -> card.color == cardOnBoard.color + } } - } + } - fun nextPlayer(direction: Direction): Player = players.elementAt(nextPlayerIndex(direction)) - - private val sidePlayerIndexClockwise: Int by lazy { - if (players.isEmpty()) { - 0 + is Card.Plus4Card -> { + if (player.cardOnBoardIsForYou && card is Card.Plus4Card) { + true } else { - ((currentPlayerIndex ?: 0) + 1) % players.size - } - } - private val sidePlayerIndexCounterClockwise: Int by lazy { - if (players.isEmpty()) { - 0 - } else { - ((currentPlayerIndex ?: 0) - 1) % players.size - } - } - - val nextPlayerTurn: Player? by lazy { - if (players.isEmpty()) { - null - } else { - nextPlayer(direction) - } - } - - private val Player.currentIndex: Int get() = players.indexOf(this) - - fun Player.playerDiffIndex(nextPlayer: Player): Int = - if (direction == Direction.CLOCKWISE) { - nextPlayer.currentIndex + this.currentIndex - } else { - nextPlayer.currentIndex - this.currentIndex - }.let { it % players.size } - - val Player.cardOnBoardIsForYou: Boolean get() { - if (cardOnCurrentStack == null) error("No card") - return this.playerDiffIndex(cardOnCurrentStack.player) == 1 - } - - fun playableCards(player: Player): List = - deck - .playersHands - .getHand(player) - ?.filter { canBePlayThisCard(player, it) } - ?: emptyList() - - fun playerHasNoCardLeft(): List = - deck.playerHasNoCardLeft().map { playerId -> - players.find { it.id == playerId } ?: error("inconsistency detected between players") - } - - fun canBePlayThisCard( - player: Player, - card: Card, - ): Boolean { - val cardOnBoard = cardOnCurrentStack?.card ?: return false - return when (cardOnBoard) { - is Card.NumericCard -> { - when (card) { - is Card.AllColorCard -> true - is Card.NumericCard -> card.number == cardOnBoard.number || card.color == cardOnBoard.color - is Card.ColorCard -> card.color == cardOnBoard.color - } - } - - is Card.ReverseCard -> { - when (card) { - is Card.ReverseCard -> true - is Card.AllColorCard -> true - is Card.ColorCard -> card.color == cardOnBoard.color - } - } - - is Card.PassCard -> { - if (player.cardOnBoardIsForYou) { - false - } else { - when (card) { - is Card.AllColorCard -> true - is Card.ColorCard -> card.color == cardOnBoard.color - } - } - } - - is Card.ChangeColorCard -> { - when (card) { - is Card.AllColorCard -> true - is Card.ColorCard -> card.color == colorOnCurrentStack - } - } - - is Card.Plus2Card -> { - if (player.cardOnBoardIsForYou && card is Card.Plus2Card) { - true - } else { - when (card) { - is Card.AllColorCard -> true - is Card.Plus2Card -> true - is Card.ColorCard -> card.color == cardOnBoard.color - } - } - } - - is Card.Plus4Card -> { - if (player.cardOnBoardIsForYou && card is Card.Plus4Card) { - true - } else { - when (card) { - is Card.AllColorCard -> true - is Card.ColorCard -> card.color == colorOnCurrentStack - } - } - } + when (card) { + is Card.AllColorCard -> true + is Card.ColorCard -> card.color == colorOnCurrentStack + } } + } } + } } diff --git a/src/main/kotlin/eventDemo/app/event/projection/GameStateBuilder.kt b/src/main/kotlin/eventDemo/app/event/projection/GameStateBuilder.kt index 0826666..aec05a9 100644 --- a/src/main/kotlin/eventDemo/app/event/projection/GameStateBuilder.kt +++ b/src/main/kotlin/eventDemo/app/event/projection/GameStateBuilder.kt @@ -13,103 +13,103 @@ import eventDemo.app.event.event.PlayerWinEvent import io.github.oshai.kotlinlogging.KotlinLogging fun GameState.apply(event: GameEvent): GameState = - this.let { state -> - val logger = KotlinLogging.logger { } - if (event is PlayerActionEvent) { - if (state.currentPlayerTurn != event.player) { - logger.atError { - message = "Inconsistent player turn. CurrentPlayerTurn: $state.currentPlayerTurn | Player: ${event.player}" - payload = - mapOf( - "CurrentPlayerTurn" to (state.currentPlayerTurn ?: "No currentPlayerTurn"), - "Player" to event.player, - ) - } - } + this.let { state -> + val logger = KotlinLogging.logger { } + if (event is PlayerActionEvent) { + if (state.currentPlayerTurn != event.player) { + logger.atError { + message = "Inconsistent player turn. CurrentPlayerTurn: $state.currentPlayerTurn | Player: ${event.player}" + payload = + mapOf( + "CurrentPlayerTurn" to (state.currentPlayerTurn ?: "No currentPlayerTurn"), + "Player" to event.player, + ) + } + } + } + + when (event) { + is CardIsPlayedEvent -> { + val nextDirectionAfterPlay = + when (event.card) { + is Card.ReverseCard -> state.direction.revert() + else -> state.direction + } + + val color = + when (event.card) { + is Card.ColorCard -> event.card.color + is Card.AllColorCard -> null + } + + val currentPlayerAfterThePlay = + if (event.card is Card.AllColorCard) { + state.currentPlayerTurn + } else { + state.nextPlayer(nextDirectionAfterPlay) + } + + state.copy( + currentPlayerTurn = currentPlayerAfterThePlay, + direction = nextDirectionAfterPlay, + colorOnCurrentStack = color, + cardOnCurrentStack = GameState.LastCard(event.card, event.player), + deck = state.deck.putOneCardFromHand(event.player, event.card), + ) + } + + is NewPlayerEvent -> { + if (state.isStarted) { + logger.error { "The game is already started" } } - when (event) { - is CardIsPlayedEvent -> { - val nextDirectionAfterPlay = - when (event.card) { - is Card.ReverseCard -> state.direction.revert() - else -> state.direction - } - - val color = - when (event.card) { - is Card.ColorCard -> event.card.color - is Card.AllColorCard -> null - } - - val currentPlayerAfterThePlay = - if (event.card is Card.AllColorCard) { - state.currentPlayerTurn - } else { - state.nextPlayer(nextDirectionAfterPlay) - } - - state.copy( - currentPlayerTurn = currentPlayerAfterThePlay, - direction = nextDirectionAfterPlay, - colorOnCurrentStack = color, - cardOnCurrentStack = GameState.LastCard(event.card, event.player), - deck = state.deck.putOneCardFromHand(event.player, event.card), - ) - } - - is NewPlayerEvent -> { - if (state.isStarted) { - logger.error { "The game is already started" } - } - - state.copy( - players = state.players + event.player, - ) - } - - is PlayerReadyEvent -> { - if (state.isStarted) { - logger.error { "The game is already started" } - } - state.copy( - readyPlayers = state.readyPlayers + event.player, - ) - } - - is PlayerHavePassEvent -> { - if (event.takenCard != state.deck.stack.first()) { - logger.error { "taken card is not ot top of the stack: ${event.takenCard}" } - } - state.copy( - currentPlayerTurn = state.nextPlayerTurn, - deck = state.deck.takeOneCardFromStackTo(event.player), - ) - } - - is PlayerChoseColorEvent -> { - state.copy( - currentPlayerTurn = state.nextPlayerTurn, - colorOnCurrentStack = event.color, - ) - } - - is GameStartedEvent -> { - state.copy( - colorOnCurrentStack = (event.deck.discard.first() as? Card.ColorCard)?.color ?: state.colorOnCurrentStack, - cardOnCurrentStack = GameState.LastCard(event.deck.discard.first(), event.firstPlayer), - currentPlayerTurn = event.firstPlayer, - deck = event.deck, - isStarted = true, - ) - } - - is PlayerWinEvent -> { - state.copy( - playerWins = state.playerWins + event.player, - ) - } - }.copy( - lastEventVersion = event.version, + state.copy( + players = state.players + event.player, ) - } + } + + is PlayerReadyEvent -> { + if (state.isStarted) { + logger.error { "The game is already started" } + } + state.copy( + readyPlayers = state.readyPlayers + event.player, + ) + } + + is PlayerHavePassEvent -> { + if (event.takenCard != state.deck.stack.first()) { + logger.error { "taken card is not ot top of the stack: ${event.takenCard}" } + } + state.copy( + currentPlayerTurn = state.nextPlayerTurn, + deck = state.deck.takeOneCardFromStackTo(event.player), + ) + } + + is PlayerChoseColorEvent -> { + state.copy( + currentPlayerTurn = state.nextPlayerTurn, + colorOnCurrentStack = event.color, + ) + } + + is GameStartedEvent -> { + state.copy( + colorOnCurrentStack = (event.deck.discard.first() as? Card.ColorCard)?.color ?: state.colorOnCurrentStack, + cardOnCurrentStack = GameState.LastCard(event.deck.discard.first(), event.firstPlayer), + currentPlayerTurn = event.firstPlayer, + deck = event.deck, + isStarted = true, + ) + } + + is PlayerWinEvent -> { + state.copy( + playerWins = state.playerWins + event.player, + ) + } + }.copy( + lastEventVersion = event.version, + ) + } diff --git a/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt b/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt index 762089d..a5f14d2 100644 --- a/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt +++ b/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt @@ -6,36 +6,38 @@ import eventDemo.app.event.GameEventStore import eventDemo.app.event.event.GameEvent class GameStateRepository( - eventStore: GameEventStore, - eventHandler: GameEventHandler, - snapshotConfig: SnapshotConfig = SnapshotConfig(), + eventStore: GameEventStore, + eventHandler: GameEventHandler, + snapshotConfig: SnapshotConfig = SnapshotConfig(), ) { - private val projectionsSnapshot = - ProjectionSnapshotRepositoryInMemory( - eventStore = eventStore, - snapshotCacheConfig = snapshotConfig, - applyToProjection = GameState::apply, - initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) }, - ) + private val projectionsSnapshot = + ProjectionSnapshotRepositoryInMemory( + eventStore = eventStore, + snapshotCacheConfig = snapshotConfig, + applyToProjection = GameState::apply, + initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) }, + ) - init { - eventHandler.registerProjectionBuilder { event -> - projectionsSnapshot.applyAndPutToCache(event) - } + init { + eventHandler.registerProjectionBuilder { event -> + projectionsSnapshot.applyAndPutToCache(event) } + } - /** - * Get the last version of the [GameState] from the all eventStream. - * - * It fetches it from the local cache if possible, otherwise it builds it. - */ - fun getLast(gameId: GameId): GameState = projectionsSnapshot.getLast(gameId) + /** + * Get the last version of the [GameState] from the all eventStream. + * + * It fetches it from the local cache if possible, otherwise it builds it. + */ + fun getLast(gameId: GameId): GameState = + projectionsSnapshot.getLast(gameId) - /** - * Get the [GameState] to the specific [event][GameEvent]. - * It does not contain the [events][GameEvent] it after this one. - * - * It fetches it from the local cache if possible, otherwise it builds it. - */ - fun getUntil(event: GameEvent): GameState = projectionsSnapshot.getUntil(event) + /** + * Get the [GameState] to the specific [event][GameEvent]. + * It does not contain the [events][GameEvent] it after this one. + * + * It fetches it from the local cache if possible, otherwise it builds it. + */ + fun getUntil(event: GameEvent): GameState = + projectionsSnapshot.getUntil(event) } diff --git a/src/main/kotlin/eventDemo/app/event/projection/Projection.kt b/src/main/kotlin/eventDemo/app/event/projection/Projection.kt index 9c4460e..4e0be8e 100644 --- a/src/main/kotlin/eventDemo/app/event/projection/Projection.kt +++ b/src/main/kotlin/eventDemo/app/event/projection/Projection.kt @@ -3,6 +3,6 @@ package eventDemo.app.event.projection import eventDemo.libs.event.AggregateId interface Projection { - val aggregateId: ID - val lastEventVersion: Int + val aggregateId: ID + val lastEventVersion: Int } diff --git a/src/main/kotlin/eventDemo/app/event/projection/ProjectionSnapshotRepositoryInMemory.kt b/src/main/kotlin/eventDemo/app/event/projection/ProjectionSnapshotRepositoryInMemory.kt index 3dd1985..80494c3 100644 --- a/src/main/kotlin/eventDemo/app/event/projection/ProjectionSnapshotRepositoryInMemory.kt +++ b/src/main/kotlin/eventDemo/app/event/projection/ProjectionSnapshotRepositoryInMemory.kt @@ -13,182 +13,183 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes data class SnapshotConfig( - val maxSnapshotCacheSize: Int = 20, - val maxSnapshotCacheTtl: Duration = 10.minutes, - /** - * Only create [snapshots][Projection] every [X][modulo] [events][Event] - */ - val modulo: Int = 10, + val maxSnapshotCacheSize: Int = 20, + val maxSnapshotCacheTtl: Duration = 10.minutes, + /** + * Only create [snapshots][Projection] every [X][modulo] [events][Event] + */ + val modulo: Int = 10, ) class ProjectionSnapshotRepositoryInMemory, P : Projection, ID : AggregateId>( - private val eventStore: EventStore, - private val initialStateBuilder: (ID) -> P, - private val snapshotCacheConfig: SnapshotConfig = SnapshotConfig(), - private val applyToProjection: P.(event: E) -> P, + private val eventStore: EventStore, + private val initialStateBuilder: (ID) -> P, + private val snapshotCacheConfig: SnapshotConfig = SnapshotConfig(), + private val applyToProjection: P.(event: E) -> P, ) { - private val projectionsSnapshot: ConcurrentHashMap>> = ConcurrentHashMap() + private val projectionsSnapshot: ConcurrentHashMap>> = ConcurrentHashMap() - /** - * Create a snapshot for the event - * - * 1. get the last snapshot with a version lower than that of the event - * 2. get the events with a greater version of the snapshot - * 3. apply the event to the snapshot - * 4. apply the new event to the projection - * 5. save it - * 6. remove old one - */ - fun applyAndPutToCache(event: E) { - if ((event.version % snapshotCacheConfig.modulo) == 0) { - getUntil(event) - .also { - save(it) - removeOldSnapshot(it.aggregateId) - } + /** + * Create a snapshot for the event + * + * 1. get the last snapshot with a version lower than that of the event + * 2. get the events with a greater version of the snapshot + * 3. apply the event to the snapshot + * 4. apply the new event to the projection + * 5. save it + * 6. remove old one + */ + fun applyAndPutToCache(event: E) { + if ((event.version % snapshotCacheConfig.modulo) == 0) { + getUntil(event) + .also { + save(it) + removeOldSnapshot(it.aggregateId) } } + } - /** - * Build the last version of the [Projection] from the cache. - * - * 1. get the last snapshot - * 2. get the missing event to the snapshot - * 3. apply the missing events to the snapshot - */ - fun getLast(aggregateId: ID): P { - val lastSnapshot = getLastSnapshot(aggregateId)?.first - val missingEventOfSnapshot = getEventAfterTheSnapshot(aggregateId, lastSnapshot) - return lastSnapshot.applyEvents(aggregateId, missingEventOfSnapshot) + /** + * Build the last version of the [Projection] from the cache. + * + * 1. get the last snapshot + * 2. get the missing event to the snapshot + * 3. apply the missing events to the snapshot + */ + fun getLast(aggregateId: ID): P { + val lastSnapshot = getLastSnapshot(aggregateId)?.first + val missingEventOfSnapshot = getEventAfterTheSnapshot(aggregateId, lastSnapshot) + return lastSnapshot.applyEvents(aggregateId, missingEventOfSnapshot) + } + + /** + * Build the [Projection] to the specific [event][Event]. + * + * It does not contain the [events][Event] it after this one. + * + * 1. get the last snapshot before the event + * 2. get the events with a greater version of the snapshot but lower of passed event + * 3. apply the events to the snapshot + */ + fun getUntil(event: E): P { + val lastSnapshot = getLastSnapshotBeforeOrEqualEvent(event)?.first + if (lastSnapshot?.lastEventVersion == event.version) { + return lastSnapshot } - /** - * Build the [Projection] to the specific [event][Event]. - * - * It does not contain the [events][Event] it after this one. - * - * 1. get the last snapshot before the event - * 2. get the events with a greater version of the snapshot but lower of passed event - * 3. apply the events to the snapshot - */ - fun getUntil(event: E): P { - val lastSnapshot = getLastSnapshotBeforeOrEqualEvent(event)?.first - if (lastSnapshot?.lastEventVersion == event.version) { - return lastSnapshot - } + val missingEventOfSnapshot = + eventStore + .getStream(event.aggregateId) + .readVersionBetween((lastSnapshot?.lastEventVersion ?: 1)..event.version) - val missingEventOfSnapshot = - eventStore - .getStream(event.aggregateId) - .readVersionBetween((lastSnapshot?.lastEventVersion ?: 1)..event.version) - - return if (lastSnapshot?.lastEventVersion == event.version) { - lastSnapshot - } else { - lastSnapshot.applyEvents(event.aggregateId, missingEventOfSnapshot) - } + return if (lastSnapshot?.lastEventVersion == event.version) { + lastSnapshot + } else { + lastSnapshot.applyEvents(event.aggregateId, missingEventOfSnapshot) } + } - /** - * Remove the oldest snapshot. - * - * The rules are pass in the controller. - */ - private fun removeOldSnapshot(aggregateId: ID) { - projectionsSnapshot[aggregateId]?.let { queue -> - // never remove the last one - val theLastOne = getLastSnapshot(aggregateId) - removeByDate(queue, theLastOne) - removeBySize(queue, theLastOne) - } + /** + * Remove the oldest snapshot. + * + * The rules are pass in the controller. + */ + private fun removeOldSnapshot(aggregateId: ID) { + projectionsSnapshot[aggregateId]?.let { queue -> + // never remove the last one + val theLastOne = getLastSnapshot(aggregateId) + removeByDate(queue, theLastOne) + removeBySize(queue, theLastOne) } + } - private fun removeBySize( - queue: ConcurrentLinkedQueue>, - theLastOne: Pair?, - ) { - // Remove if size exceeds the limit - val size = queue.size - if (size > snapshotCacheConfig.maxSnapshotCacheSize) { - val numberToRemove = size - snapshotCacheConfig.maxSnapshotCacheSize - if (numberToRemove > 0) { - queue - .sortedBy { it.first.lastEventVersion } - .take(numberToRemove) - .let { it - theLastOne } - .forEach { queue.remove(it) } - } - } + private fun removeBySize( + queue: ConcurrentLinkedQueue>, + theLastOne: Pair?, + ) { + // Remove if size exceeds the limit + val size = queue.size + if (size > snapshotCacheConfig.maxSnapshotCacheSize) { + val numberToRemove = size - snapshotCacheConfig.maxSnapshotCacheSize + if (numberToRemove > 0) { + queue + .sortedBy { it.first.lastEventVersion } + .take(numberToRemove) + .let { it - theLastOne } + .forEach { queue.remove(it) } + } } + } - private fun removeByDate( - queue: ConcurrentLinkedQueue>, - theLastOne: Pair?, - ) { - // remove the oldest by time - val now = Clock.System.now() - val deadLine = now - snapshotCacheConfig.maxSnapshotCacheTtl - val toRemove = queue.filter { deadLine > it.second } - (toRemove - theLastOne).forEach { queue.remove(it) } - } - - /** - * Save the snapshot. - */ - private fun save(projection: P) { - projectionsSnapshot - .computeIfAbsent(projection.aggregateId) { ConcurrentLinkedQueue() } - .add(Pair(projection, Clock.System.now())) - } - - /** - * Get the last snapshot when the version is lower of then event version - */ - private fun getLastSnapshotBeforeOrEqualEvent(event: E) = - projectionsSnapshot[event.aggregateId] - ?.sortedByDescending { it.first.lastEventVersion } - ?.find { it.first.lastEventVersion <= event.version } - - /** - * Get the last snapshot (with the higher version). - */ - private fun getLastSnapshot(aggregateId: ID) = - projectionsSnapshot[aggregateId] - ?.maxByOrNull { it.first.lastEventVersion } - - /** - * Get the events from the [event stream][EventStream] when the version is higher of the snapshot. - * - * If the snapshot is null, it takes all events from the event [event stream][EventStream] - */ - private fun getEventAfterTheSnapshot( - aggregateId: ID, - snapshot: P?, - ) = eventStore - .getStream(aggregateId) - .readGreaterOfVersion(snapshot?.lastEventVersion ?: 0) - - /** - * Apply events to the projection. - */ - private fun P?.applyEvents( - aggregateId: ID, - eventsToApply: Set, - ): P = - eventsToApply - .fold(this ?: initialStateBuilder(aggregateId), applyToProjectionSecure) - - /** - * Wrap the [applyToProjection] lambda to avoid duplicate apply of the same event. - */ - private val applyToProjectionSecure: P.(event: E) -> P = { event -> - if (event.version == lastEventVersion + 1) { - applyToProjection(event) - } else if (event.version <= lastEventVersion) { - KotlinLogging.logger { }.warn { "Event is already is the Projection, skip apply." } - this - } else { - error("The version of the event must follow directly after the version of the projection.") - } + private fun removeByDate( + queue: ConcurrentLinkedQueue>, + theLastOne: Pair?, + ) { + // remove the oldest by time + val now = Clock.System.now() + val deadLine = now - snapshotCacheConfig.maxSnapshotCacheTtl + val toRemove = queue.filter { deadLine > it.second } + (toRemove - theLastOne).forEach { queue.remove(it) } + } + + /** + * Save the snapshot. + */ + private fun save(projection: P) { + projectionsSnapshot + .computeIfAbsent(projection.aggregateId) { ConcurrentLinkedQueue() } + .add(Pair(projection, Clock.System.now())) + } + + /** + * Get the last snapshot when the version is lower of then event version + */ + private fun getLastSnapshotBeforeOrEqualEvent(event: E) = + projectionsSnapshot[event.aggregateId] + ?.sortedByDescending { it.first.lastEventVersion } + ?.find { it.first.lastEventVersion <= event.version } + + /** + * Get the last snapshot (with the higher version). + */ + private fun getLastSnapshot(aggregateId: ID) = + projectionsSnapshot[aggregateId] + ?.maxByOrNull { it.first.lastEventVersion } + + /** + * Get the events from the [event stream][EventStream] when the version is higher of the snapshot. + * + * If the snapshot is null, it takes all events from the event [event stream][EventStream] + */ + private fun getEventAfterTheSnapshot( + aggregateId: ID, + snapshot: P?, + ) = + eventStore + .getStream(aggregateId) + .readGreaterOfVersion(snapshot?.lastEventVersion ?: 0) + + /** + * Apply events to the projection. + */ + private fun P?.applyEvents( + aggregateId: ID, + eventsToApply: Set, + ): P = + eventsToApply + .fold(this ?: initialStateBuilder(aggregateId), applyToProjectionSecure) + + /** + * Wrap the [applyToProjection] lambda to avoid duplicate apply of the same event. + */ + private val applyToProjectionSecure: P.(event: E) -> P = { event -> + if (event.version == lastEventVersion + 1) { + applyToProjection(event) + } else if (event.version <= lastEventVersion) { + KotlinLogging.logger { }.warn { "Event is already is the Projection, skip apply." } + this + } else { + error("The version of the event must follow directly after the version of the projection.") } + } } diff --git a/src/main/kotlin/eventDemo/app/eventListener/PlayerNotificationEventListener.kt b/src/main/kotlin/eventDemo/app/eventListener/PlayerNotificationEventListener.kt index 14b8cb1..7acf5a1 100644 --- a/src/main/kotlin/eventDemo/app/eventListener/PlayerNotificationEventListener.kt +++ b/src/main/kotlin/eventDemo/app/eventListener/PlayerNotificationEventListener.kt @@ -28,118 +28,118 @@ import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.trySendBlocking class PlayerNotificationEventListener( - private val eventBus: GameEventBus, - private val gameStateRepository: GameStateRepository, + private val eventBus: GameEventBus, + private val gameStateRepository: GameStateRepository, ) { - private val logger = KotlinLogging.logger {} + private val logger = KotlinLogging.logger {} - fun startListening( - outgoingNotificationChannel: SendChannel, - currentPlayer: Player, - ) { - eventBus.subscribe { event: GameEvent -> - val currentState = gameStateRepository.getUntil(event) + fun startListening( + outgoingNotificationChannel: SendChannel, + currentPlayer: Player, + ) { + eventBus.subscribe { event: GameEvent -> + val currentState = gameStateRepository.getUntil(event) - fun Notification.send() { - if (currentState.players.contains(currentPlayer)) { - // Only notify players who have already joined the game. - outgoingNotificationChannel.trySendBlocking(this) - logger.atInfo { - message = "Notification for player ${currentPlayer.name} was SEND: ${this@send}" - payload = mapOf("notification" to this@send, "event" to event) - } - } else { - // Rare use case, when a connexion is created with the channel, - // but the player was not already join in the game - logger.atWarn { - message = "Notification for player ${currentPlayer.name} was SKIP, No player on the game: ${this@send}" - payload = mapOf("notification" to this@send, "event" to event) - } - } - } - - fun sendNextTurnNotif() = - ItsTheTurnOfNotification( - player = currentState.currentPlayerTurn ?: error("No player turn defined"), - ).send() - - when (event) { - is NewPlayerEvent -> { - if (currentPlayer != event.player) { - PlayerAsJoinTheGameNotification( - player = event.player, - ).send() - } else { - WelcomeToTheGameNotification( - players = currentState.players, - ).send() - } - } - - is CardIsPlayedEvent -> { - if (currentPlayer != event.player) { - PlayerAsPlayACardNotification( - player = event.player, - card = event.card, - ).send() - } - - if (event.card !is Card.AllColorCard) { - ItsTheTurnOfNotification( - player = currentState.currentPlayerTurn ?: error("No player turn defined"), - ).send() - } - } - - is GameStartedEvent -> { - TheGameWasStartedNotification( - hand = - event.deck.playersHands.getHand(currentPlayer) - ?: error("You are not in the game"), - ).send() - - sendNextTurnNotif() - } - - is PlayerChoseColorEvent -> { - if (currentPlayer != event.player) { - PlayerWasChoseTheCardColorNotification( - player = event.player, - color = event.color, - ).send() - } - - sendNextTurnNotif() - } - - is PlayerHavePassEvent -> { - if (currentPlayer == event.player) { - YourNewCardNotification( - card = event.takenCard, - ).send() - } else { - PlayerHavePassNotification( - player = event.player, - ).send() - } - - sendNextTurnNotif() - } - - is PlayerReadyEvent -> { - if (currentPlayer != event.player) { - PlayerWasReadyNotification( - player = event.player, - ).send() - } - } - - is PlayerWinEvent -> { - PlayerWinNotification( - player = event.player, - ).send() - } - } + fun Notification.send() { + if (currentState.players.contains(currentPlayer)) { + // Only notify players who have already joined the game. + outgoingNotificationChannel.trySendBlocking(this) + logger.atInfo { + message = "Notification for player ${currentPlayer.name} was SEND: ${this@send}" + payload = mapOf("notification" to this@send, "event" to event) + } + } else { + // Rare use case, when a connexion is created with the channel, + // but the player was not already join in the game + logger.atWarn { + message = "Notification for player ${currentPlayer.name} was SKIP, No player on the game: ${this@send}" + payload = mapOf("notification" to this@send, "event" to event) + } } + } + + fun sendNextTurnNotif() = + ItsTheTurnOfNotification( + player = currentState.currentPlayerTurn ?: error("No player turn defined"), + ).send() + + when (event) { + is NewPlayerEvent -> { + if (currentPlayer != event.player) { + PlayerAsJoinTheGameNotification( + player = event.player, + ).send() + } else { + WelcomeToTheGameNotification( + players = currentState.players, + ).send() + } + } + + is CardIsPlayedEvent -> { + if (currentPlayer != event.player) { + PlayerAsPlayACardNotification( + player = event.player, + card = event.card, + ).send() + } + + if (event.card !is Card.AllColorCard) { + ItsTheTurnOfNotification( + player = currentState.currentPlayerTurn ?: error("No player turn defined"), + ).send() + } + } + + is GameStartedEvent -> { + TheGameWasStartedNotification( + hand = + event.deck.playersHands.getHand(currentPlayer) + ?: error("You are not in the game"), + ).send() + + sendNextTurnNotif() + } + + is PlayerChoseColorEvent -> { + if (currentPlayer != event.player) { + PlayerWasChoseTheCardColorNotification( + player = event.player, + color = event.color, + ).send() + } + + sendNextTurnNotif() + } + + is PlayerHavePassEvent -> { + if (currentPlayer == event.player) { + YourNewCardNotification( + card = event.takenCard, + ).send() + } else { + PlayerHavePassNotification( + player = event.player, + ).send() + } + + sendNextTurnNotif() + } + + is PlayerReadyEvent -> { + if (currentPlayer != event.player) { + PlayerWasReadyNotification( + player = event.player, + ).send() + } + } + + is PlayerWinEvent -> { + PlayerWinNotification( + player = event.player, + ).send() + } + } } + } } diff --git a/src/main/kotlin/eventDemo/app/eventListener/ReactionEventListener.kt b/src/main/kotlin/eventDemo/app/eventListener/ReactionEventListener.kt index 0cfc9f9..7b355eb 100644 --- a/src/main/kotlin/eventDemo/app/eventListener/ReactionEventListener.kt +++ b/src/main/kotlin/eventDemo/app/eventListener/ReactionEventListener.kt @@ -11,76 +11,76 @@ import eventDemo.app.event.projection.GameStateRepository import io.github.oshai.kotlinlogging.KotlinLogging class ReactionEventListener( - private val eventBus: GameEventBus, - private val eventHandler: GameEventHandler, - private val gameStateRepository: GameStateRepository, - private val priority: Int = DEFAULT_PRIORITY, + private val eventBus: GameEventBus, + private val eventHandler: GameEventHandler, + private val gameStateRepository: GameStateRepository, + private val priority: Int = DEFAULT_PRIORITY, ) { - companion object Config { - const val DEFAULT_PRIORITY = -1000 + companion object Config { + const val DEFAULT_PRIORITY = -1000 + } + + private val logger = KotlinLogging.logger { } + + fun init() { + eventBus.subscribe(priority) { event: GameEvent -> + val state = gameStateRepository.getUntil(event) + sendStartGameEvent(state, event) + sendWinnerEvent(state, event) } + } - private val logger = KotlinLogging.logger { } - - fun init() { - eventBus.subscribe(priority) { event: GameEvent -> - val state = gameStateRepository.getUntil(event) - sendStartGameEvent(state, event) - sendWinnerEvent(state, event) + private suspend fun sendStartGameEvent( + state: GameState, + event: GameEvent, + ) { + if (state.isReady && !state.isStarted) { + val reactionEvent = + eventHandler.handle(state.aggregateId) { + GameStartedEvent.new( + id = state.aggregateId, + players = state.players, + version = it, + ) } + logger.atInfo { + message = "Reaction event was Send $reactionEvent on reaction of: $event" + payload = + mapOf( + "event" to event, + "reactionEvent" to reactionEvent, + ) + } + } else { + if (event is PlayerReadyEvent) { + logger.info { "All players was not ready ${state.readyPlayers}" } + } } + } - private suspend fun sendStartGameEvent( - state: GameState, - event: GameEvent, - ) { - if (state.isReady && !state.isStarted) { - val reactionEvent = - eventHandler.handle(state.aggregateId) { - GameStartedEvent.new( - id = state.aggregateId, - players = state.players, - version = it, - ) - } - logger.atInfo { - message = "Reaction event was Send $reactionEvent on reaction of: $event" - payload = - mapOf( - "event" to event, - "reactionEvent" to reactionEvent, - ) - } - } else { - if (event is PlayerReadyEvent) { - logger.info { "All players was not ready ${state.readyPlayers}" } - } + private fun sendWinnerEvent( + state: GameState, + event: GameEvent, + ) { + val winner = state.playerHasNoCardLeft().firstOrNull() + if (winner != null) { + val reactionEvent = + eventHandler.handle(state.aggregateId) { + PlayerWinEvent( + aggregateId = state.aggregateId, + player = winner, + version = it, + ) } - } - private fun sendWinnerEvent( - state: GameState, - event: GameEvent, - ) { - val winner = state.playerHasNoCardLeft().firstOrNull() - if (winner != null) { - val reactionEvent = - eventHandler.handle(state.aggregateId) { - PlayerWinEvent( - aggregateId = state.aggregateId, - player = winner, - version = it, - ) - } - - logger.atInfo { - message = "Reaction event was Send $reactionEvent on reaction of: $event" - payload = - mapOf( - "event" to event, - "reactionEvent" to reactionEvent, - ) - } - } + logger.atInfo { + message = "Reaction event was Send $reactionEvent on reaction of: $event" + payload = + mapOf( + "event" to event, + "reactionEvent" to reactionEvent, + ) + } } + } } diff --git a/src/main/kotlin/eventDemo/app/notification/ErrorNotification.kt b/src/main/kotlin/eventDemo/app/notification/ErrorNotification.kt index 28a12e0..bc4c83e 100644 --- a/src/main/kotlin/eventDemo/app/notification/ErrorNotification.kt +++ b/src/main/kotlin/eventDemo/app/notification/ErrorNotification.kt @@ -6,7 +6,7 @@ import java.util.UUID @Serializable data class ErrorNotification( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - val message: String, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + val message: String, ) : Notification diff --git a/src/main/kotlin/eventDemo/app/notification/ItsTheTurnOfNotification.kt b/src/main/kotlin/eventDemo/app/notification/ItsTheTurnOfNotification.kt index 7ace0c5..98df5a9 100644 --- a/src/main/kotlin/eventDemo/app/notification/ItsTheTurnOfNotification.kt +++ b/src/main/kotlin/eventDemo/app/notification/ItsTheTurnOfNotification.kt @@ -7,7 +7,7 @@ import java.util.UUID @Serializable data class ItsTheTurnOfNotification( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - val player: Player, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + val player: Player, ) : Notification diff --git a/src/main/kotlin/eventDemo/app/notification/Notification.kt b/src/main/kotlin/eventDemo/app/notification/Notification.kt index d0172d8..ad992c3 100644 --- a/src/main/kotlin/eventDemo/app/notification/Notification.kt +++ b/src/main/kotlin/eventDemo/app/notification/Notification.kt @@ -6,6 +6,6 @@ import java.util.UUID @Serializable sealed interface Notification { - @Serializable(with = UUIDSerializer::class) - val id: UUID + @Serializable(with = UUIDSerializer::class) + val id: UUID } diff --git a/src/main/kotlin/eventDemo/app/notification/PlayerAsJoinTheGameNotification.kt b/src/main/kotlin/eventDemo/app/notification/PlayerAsJoinTheGameNotification.kt index 80b37ca..cb1e5f6 100644 --- a/src/main/kotlin/eventDemo/app/notification/PlayerAsJoinTheGameNotification.kt +++ b/src/main/kotlin/eventDemo/app/notification/PlayerAsJoinTheGameNotification.kt @@ -7,7 +7,7 @@ import java.util.UUID @Serializable data class PlayerAsJoinTheGameNotification( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - val player: Player, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + val player: Player, ) : Notification diff --git a/src/main/kotlin/eventDemo/app/notification/PlayerAsPlayACardNotification.kt b/src/main/kotlin/eventDemo/app/notification/PlayerAsPlayACardNotification.kt index fd55e84..19c3a99 100644 --- a/src/main/kotlin/eventDemo/app/notification/PlayerAsPlayACardNotification.kt +++ b/src/main/kotlin/eventDemo/app/notification/PlayerAsPlayACardNotification.kt @@ -8,8 +8,8 @@ import java.util.UUID @Serializable data class PlayerAsPlayACardNotification( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - val player: Player, - val card: Card, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + val player: Player, + val card: Card, ) : Notification diff --git a/src/main/kotlin/eventDemo/app/notification/PlayerHavePassNotification.kt b/src/main/kotlin/eventDemo/app/notification/PlayerHavePassNotification.kt index cfb4fe3..e681b52 100644 --- a/src/main/kotlin/eventDemo/app/notification/PlayerHavePassNotification.kt +++ b/src/main/kotlin/eventDemo/app/notification/PlayerHavePassNotification.kt @@ -7,7 +7,7 @@ import java.util.UUID @Serializable data class PlayerHavePassNotification( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - val player: Player, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + val player: Player, ) : Notification diff --git a/src/main/kotlin/eventDemo/app/notification/PlayerWasChoseTheCardColorNotification.kt b/src/main/kotlin/eventDemo/app/notification/PlayerWasChoseTheCardColorNotification.kt index 164fe00..b7c637a 100644 --- a/src/main/kotlin/eventDemo/app/notification/PlayerWasChoseTheCardColorNotification.kt +++ b/src/main/kotlin/eventDemo/app/notification/PlayerWasChoseTheCardColorNotification.kt @@ -8,8 +8,8 @@ import java.util.UUID @Serializable data class PlayerWasChoseTheCardColorNotification( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - val player: Player, - val color: Card.Color, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + val player: Player, + val color: Card.Color, ) : Notification diff --git a/src/main/kotlin/eventDemo/app/notification/PlayerWasReadyNotification.kt b/src/main/kotlin/eventDemo/app/notification/PlayerWasReadyNotification.kt index 64e220c..789873a 100644 --- a/src/main/kotlin/eventDemo/app/notification/PlayerWasReadyNotification.kt +++ b/src/main/kotlin/eventDemo/app/notification/PlayerWasReadyNotification.kt @@ -7,7 +7,7 @@ import java.util.UUID @Serializable data class PlayerWasReadyNotification( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - val player: Player, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + val player: Player, ) : Notification diff --git a/src/main/kotlin/eventDemo/app/notification/PlayerWinNotification.kt b/src/main/kotlin/eventDemo/app/notification/PlayerWinNotification.kt index 00e0d1a..d4cb29b 100644 --- a/src/main/kotlin/eventDemo/app/notification/PlayerWinNotification.kt +++ b/src/main/kotlin/eventDemo/app/notification/PlayerWinNotification.kt @@ -7,7 +7,7 @@ import java.util.UUID @Serializable data class PlayerWinNotification( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - val player: Player, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + val player: Player, ) : Notification diff --git a/src/main/kotlin/eventDemo/app/notification/TheGameWasStartedNotification.kt b/src/main/kotlin/eventDemo/app/notification/TheGameWasStartedNotification.kt index c848e1c..b551290 100644 --- a/src/main/kotlin/eventDemo/app/notification/TheGameWasStartedNotification.kt +++ b/src/main/kotlin/eventDemo/app/notification/TheGameWasStartedNotification.kt @@ -7,7 +7,7 @@ import java.util.UUID @Serializable data class TheGameWasStartedNotification( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - val hand: List, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + val hand: List, ) : Notification diff --git a/src/main/kotlin/eventDemo/app/notification/WelcomeToTheGameNotification.kt b/src/main/kotlin/eventDemo/app/notification/WelcomeToTheGameNotification.kt index 9c0db5f..66ed102 100644 --- a/src/main/kotlin/eventDemo/app/notification/WelcomeToTheGameNotification.kt +++ b/src/main/kotlin/eventDemo/app/notification/WelcomeToTheGameNotification.kt @@ -7,7 +7,7 @@ import java.util.UUID @Serializable data class WelcomeToTheGameNotification( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - val players: Set, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + val players: Set, ) : Notification diff --git a/src/main/kotlin/eventDemo/app/notification/YourNewCardNotification.kt b/src/main/kotlin/eventDemo/app/notification/YourNewCardNotification.kt index da7765c..a02150f 100644 --- a/src/main/kotlin/eventDemo/app/notification/YourNewCardNotification.kt +++ b/src/main/kotlin/eventDemo/app/notification/YourNewCardNotification.kt @@ -7,7 +7,7 @@ import java.util.UUID @Serializable data class YourNewCardNotification( - @Serializable(with = UUIDSerializer::class) - override val id: UUID = UUID.randomUUID(), - val card: Card, + @Serializable(with = UUIDSerializer::class) + override val id: UUID = UUID.randomUUID(), + val card: Card, ) : Notification diff --git a/src/main/kotlin/eventDemo/app/query/ReadTheGameState.kt b/src/main/kotlin/eventDemo/app/query/ReadTheGameState.kt index 73f2871..0c16e72 100644 --- a/src/main/kotlin/eventDemo/app/query/ReadTheGameState.kt +++ b/src/main/kotlin/eventDemo/app/query/ReadTheGameState.kt @@ -15,41 +15,41 @@ import kotlinx.serialization.Serializable @Serializable @Resource("/game/{id}") class Game( - @Serializable(with = GameIdSerializer::class) - val id: GameId, + @Serializable(with = GameIdSerializer::class) + val id: GameId, ) { - @Serializable - @Resource("card/last") - class Card( - val game: Game, - ) + @Serializable + @Resource("card/last") + class Card( + val game: Game, + ) - @Serializable - @Resource("state") - class State( - val game: Game, - ) + @Serializable + @Resource("state") + class State( + val game: Game, + ) } /** * API routes to read the game state. */ fun Route.readTheGameState(gameStateRepository: GameStateRepository) { - authenticate { - // Read the last played card on the game. - get { body -> - gameStateRepository - .getLast(body.game.id) - .cardOnCurrentStack - ?.card - ?.let { call.respond(it) } - ?: call.response.status(HttpStatusCode.BadRequest) - } - - // Read the last played card on the game. - get { body -> - val state = gameStateRepository.getLast(body.game.id) - call.respond(state) - } + authenticate { + // Read the last played card on the game. + get { body -> + gameStateRepository + .getLast(body.game.id) + .cardOnCurrentStack + ?.card + ?.let { call.respond(it) } + ?: call.response.status(HttpStatusCode.BadRequest) } + + // Read the last played card on the game. + get { body -> + val state = gameStateRepository.getLast(body.game.id) + call.respond(state) + } + } } diff --git a/src/main/kotlin/eventDemo/configuration/Configure.kt b/src/main/kotlin/eventDemo/configuration/Configure.kt index 5a1a2a4..a403143 100644 --- a/src/main/kotlin/eventDemo/configuration/Configure.kt +++ b/src/main/kotlin/eventDemo/configuration/Configure.kt @@ -4,17 +4,17 @@ import io.ktor.server.application.Application import org.koin.ktor.ext.get fun Application.configure() { - configureKoin() + configureKoin() - configureSecurity() + configureSecurity() - configureSerialization() + configureSerialization() - configureWebSockets() - declareWebSocketsGameRoute(get(), get()) + configureWebSockets() + declareWebSocketsGameRoute(get(), get()) - configureHttpRouting() - declareHttpGameRoute() + configureHttpRouting() + declareHttpGameRoute() - configureGameListener() + configureGameListener() } diff --git a/src/main/kotlin/eventDemo/configuration/ConfigureAuth.kt b/src/main/kotlin/eventDemo/configuration/ConfigureAuth.kt index dbda225..2907ccb 100644 --- a/src/main/kotlin/eventDemo/configuration/ConfigureAuth.kt +++ b/src/main/kotlin/eventDemo/configuration/ConfigureAuth.kt @@ -21,43 +21,43 @@ private val jwtIssuer = "PlayCardGame" private val jwtSecret = "secret" fun Application.configureSecurity() { - authentication { - jwt { - realm = jwtRealm - verifier( - JWT - .require(Algorithm.HMAC256(jwtSecret)) - .withIssuer(jwtIssuer) - .build(), - ) - validate { credential -> - if (credential.payload.getClaim("username").asString() != "") { - JWTPrincipal(credential.payload) - } else { - null - } - } - challenge { defaultScheme, realm -> - call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired") - } + authentication { + jwt { + realm = jwtRealm + verifier( + JWT + .require(Algorithm.HMAC256(jwtSecret)) + .withIssuer(jwtIssuer) + .build(), + ) + validate { credential -> + if (credential.payload.getClaim("username").asString() != "") { + JWTPrincipal(credential.payload) + } else { + null } + } + challenge { defaultScheme, realm -> + call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired") + } } + } - routing { - post("login/{username}") { - val username = call.parameters["username"]!! - val player = Player(name = username) + routing { + post("login/{username}") { + val username = call.parameters["username"]!! + val player = Player(name = username) - call.respond(hashMapOf("token" to player.makeJwt())) - } + call.respond(hashMapOf("token" to player.makeJwt())) } + } } fun Player.makeJwt(): String = - JWT - .create() - .withIssuer(jwtIssuer) - .withClaim("username", name) - .withPayload(Json.encodeToString(this)) - .withExpiresAt(Date(System.currentTimeMillis() + 60000)) - .sign(Algorithm.HMAC256(jwtSecret)) + JWT + .create() + .withIssuer(jwtIssuer) + .withClaim("username", name) + .withPayload(Json.encodeToString(this)) + .withExpiresAt(Date(System.currentTimeMillis() + 60000)) + .sign(Algorithm.HMAC256(jwtSecret)) diff --git a/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt b/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt index a33f723..e0d9030 100644 --- a/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt +++ b/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt @@ -23,30 +23,30 @@ import org.koin.ktor.plugin.Koin import org.koin.logger.slf4jLogger fun Application.configureKoin() { - install(Koin) { - slf4jLogger() - modules(appKoinModule) - } + install(Koin) { + slf4jLogger() + modules(appKoinModule) + } } val appKoinModule = - module { - single { - GameEventBus(EventBusInMemory()) - } - single { - GameEventStore(EventStoreInMemory()) - } - single { - GameStateRepository(get(), get(), snapshotConfig = SnapshotConfig()) - } - single { - CommandStreamChannelBuilder() - } - - singleOf(::VersionBuilderLocal) bind VersionBuilder::class - singleOf(::GameEventHandler) - singleOf(::GameCommandRunner) - singleOf(::GameCommandHandler) - singleOf(::PlayerNotificationEventListener) + module { + single { + GameEventBus(EventBusInMemory()) } + single { + GameEventStore(EventStoreInMemory()) + } + single { + GameStateRepository(get(), get(), snapshotConfig = SnapshotConfig()) + } + single { + CommandStreamChannelBuilder() + } + + singleOf(::VersionBuilderLocal) bind VersionBuilder::class + singleOf(::GameEventHandler) + singleOf(::GameCommandRunner) + singleOf(::GameCommandHandler) + singleOf(::PlayerNotificationEventListener) + } diff --git a/src/main/kotlin/eventDemo/configuration/ConfigureGameListener.kt b/src/main/kotlin/eventDemo/configuration/ConfigureGameListener.kt index 27f4889..2743be9 100644 --- a/src/main/kotlin/eventDemo/configuration/ConfigureGameListener.kt +++ b/src/main/kotlin/eventDemo/configuration/ConfigureGameListener.kt @@ -5,6 +5,6 @@ import io.ktor.server.application.Application import org.koin.ktor.ext.get fun Application.configureGameListener() { - ReactionEventListener(get(), get(), get()) - .init() + ReactionEventListener(get(), get(), get()) + .init() } diff --git a/src/main/kotlin/eventDemo/configuration/ConfigureHttp.kt b/src/main/kotlin/eventDemo/configuration/ConfigureHttp.kt index 367089d..d34e1c6 100644 --- a/src/main/kotlin/eventDemo/configuration/ConfigureHttp.kt +++ b/src/main/kotlin/eventDemo/configuration/ConfigureHttp.kt @@ -12,41 +12,41 @@ import io.ktor.server.resources.Resources import io.ktor.server.response.respondText fun Application.configureHttpRouting() { - install(CORS) { - allowMethod(HttpMethod.Options) - allowMethod(HttpMethod.Put) - allowMethod(HttpMethod.Post) - allowMethod(HttpMethod.Delete) - allowMethod(HttpMethod.Patch) - allowHeader(HttpHeaders.Authorization) - allowHeader("MyCustomHeader") - anyHost() // @TODO: Don't do this in production if possible. Try to limit it. + install(CORS) { + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Post) + allowMethod(HttpMethod.Delete) + allowMethod(HttpMethod.Patch) + allowHeader(HttpHeaders.Authorization) + allowHeader("MyCustomHeader") + anyHost() // @TODO: Don't do this in production if possible. Try to limit it. + } + install(AutoHeadResponse) + install(Resources) + install(StatusPages) { + exception { call, cause -> + call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest) } - install(AutoHeadResponse) - install(Resources) - install(StatusPages) { - exception { call, cause -> - call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest) - } - exception { call, cause -> - call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError) - } + exception { call, cause -> + call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError) } + } } class BadRequestException( - val httpError: HttpErrorBadRequest, + val httpError: HttpErrorBadRequest, ) : Exception() class HttpErrorBadRequest( - statusCode: HttpStatusCode, - val title: String = statusCode.description, - val invalidParams: List, + statusCode: HttpStatusCode, + val title: String = statusCode.description, + val invalidParams: List, ) { - val statusCode: Int = statusCode.value + val statusCode: Int = statusCode.value - data class InvalidParam( - val name: String, - val reason: String, - ) + data class InvalidParam( + val name: String, + val reason: String, + ) } diff --git a/src/main/kotlin/eventDemo/configuration/ConfigureSerialization.kt b/src/main/kotlin/eventDemo/configuration/ConfigureSerialization.kt index 622b7c3..952bc94 100644 --- a/src/main/kotlin/eventDemo/configuration/ConfigureSerialization.kt +++ b/src/main/kotlin/eventDemo/configuration/ConfigureSerialization.kt @@ -18,72 +18,76 @@ import kotlinx.serialization.modules.SerializersModule import java.util.UUID fun Application.configureSerialization() { - install(ContentNegotiation) { - json( - defaultJsonSerializer(), - ) - } + install(ContentNegotiation) { + json( + defaultJsonSerializer(), + ) + } } fun defaultJsonSerializer(): Json = - Json { - serializersModule = - SerializersModule { - contextual(UUID::class) { UUIDSerializer } - contextual(GameId::class) { GameIdSerializer } - contextual(CommandId::class) { CommandIdSerializer } - contextual(Player.PlayerId::class) { PlayerIdSerializer } - } - } + Json { + serializersModule = + SerializersModule { + contextual(UUID::class) { UUIDSerializer } + contextual(GameId::class) { GameIdSerializer } + contextual(CommandId::class) { CommandIdSerializer } + contextual(Player.PlayerId::class) { PlayerIdSerializer } + } + } object CommandIdSerializer : KSerializer { - override fun deserialize(decoder: Decoder): CommandId = CommandId(decoder.decodeString()) + override fun deserialize(decoder: Decoder): CommandId = + CommandId(decoder.decodeString()) - override fun serialize( - encoder: Encoder, - value: CommandId, - ) { - encoder.encodeString(value.toString()) - } + override fun serialize( + encoder: Encoder, + value: CommandId, + ) { + encoder.encodeString(value.toString()) + } - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CommandId", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CommandId", PrimitiveKind.STRING) } object PlayerIdSerializer : KSerializer { - override fun deserialize(decoder: Decoder): Player.PlayerId = Player.PlayerId(UUID.fromString(decoder.decodeString())) + override fun deserialize(decoder: Decoder): Player.PlayerId = + Player.PlayerId(UUID.fromString(decoder.decodeString())) - override fun serialize( - encoder: Encoder, - value: Player.PlayerId, - ) { - encoder.encodeString(value.id.toString()) - } + override fun serialize( + encoder: Encoder, + value: Player.PlayerId, + ) { + encoder.encodeString(value.id.toString()) + } - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PlayerId", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PlayerId", PrimitiveKind.STRING) } object GameIdSerializer : KSerializer { - override fun deserialize(decoder: Decoder): GameId = GameId(UUID.fromString(decoder.decodeString())) + override fun deserialize(decoder: Decoder): GameId = + GameId(UUID.fromString(decoder.decodeString())) - override fun serialize( - encoder: Encoder, - value: GameId, - ) { - encoder.encodeString(value.id.toString()) - } + override fun serialize( + encoder: Encoder, + value: GameId, + ) { + encoder.encodeString(value.id.toString()) + } - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("GameId", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("GameId", PrimitiveKind.STRING) } object UUIDSerializer : KSerializer { - override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString()) + override fun deserialize(decoder: Decoder): UUID = + UUID.fromString(decoder.decodeString()) - override fun serialize( - encoder: Encoder, - value: UUID, - ) { - encoder.encodeString(value.toString()) - } + override fun serialize( + encoder: Encoder, + value: UUID, + ) { + encoder.encodeString(value.toString()) + } - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) } diff --git a/src/main/kotlin/eventDemo/configuration/ConfigureWebSockets.kt b/src/main/kotlin/eventDemo/configuration/ConfigureWebSockets.kt index 8dbd209..b759f84 100644 --- a/src/main/kotlin/eventDemo/configuration/ConfigureWebSockets.kt +++ b/src/main/kotlin/eventDemo/configuration/ConfigureWebSockets.kt @@ -8,10 +8,10 @@ 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 - } + install(WebSockets) { + pingPeriod = Duration.ofSeconds(15) + timeout = Duration.ofSeconds(15) + maxFrameSize = Long.MAX_VALUE + masking = false + } } diff --git a/src/main/kotlin/eventDemo/configuration/ConfigureWebSocketsGameRoute.kt b/src/main/kotlin/eventDemo/configuration/ConfigureWebSocketsGameRoute.kt index 16c973e..97df84b 100644 --- a/src/main/kotlin/eventDemo/configuration/ConfigureWebSocketsGameRoute.kt +++ b/src/main/kotlin/eventDemo/configuration/ConfigureWebSocketsGameRoute.kt @@ -9,10 +9,10 @@ import kotlinx.coroutines.DelicateCoroutinesApi @OptIn(DelicateCoroutinesApi::class) fun Application.declareWebSocketsGameRoute( - playerNotificationListener: PlayerNotificationEventListener, - commandHandler: GameCommandHandler, + playerNotificationListener: PlayerNotificationEventListener, + commandHandler: GameCommandHandler, ) { - routing { - gameSocket(playerNotificationListener, commandHandler) - } + routing { + gameSocket(playerNotificationListener, commandHandler) + } } diff --git a/src/main/kotlin/eventDemo/configuration/DeclareHttpRoutes.kt b/src/main/kotlin/eventDemo/configuration/DeclareHttpRoutes.kt index b8454cf..61b4aa1 100644 --- a/src/main/kotlin/eventDemo/configuration/DeclareHttpRoutes.kt +++ b/src/main/kotlin/eventDemo/configuration/DeclareHttpRoutes.kt @@ -6,7 +6,7 @@ import io.ktor.server.routing.routing import org.koin.ktor.ext.get fun Application.declareHttpGameRoute() { - routing { - readTheGameState(get()) - } + routing { + readTheGameState(get()) + } } diff --git a/src/main/kotlin/eventDemo/libs/FrameChannelConverter.kt b/src/main/kotlin/eventDemo/libs/FrameChannelConverter.kt index db08d46..16f3f65 100644 --- a/src/main/kotlin/eventDemo/libs/FrameChannelConverter.kt +++ b/src/main/kotlin/eventDemo/libs/FrameChannelConverter.kt @@ -16,28 +16,28 @@ import kotlinx.serialization.json.Json @OptIn(ExperimentalCoroutinesApi::class, InternalCoroutinesApi::class) inline fun CoroutineScope.toObjectChannel( - frames: ReceiveChannel, - bufferSize: Int = 0, + frames: ReceiveChannel, + bufferSize: Int = 0, ): ReceiveChannel { - val logger = KotlinLogging.logger { } - return produce(capacity = bufferSize) { - frames.consumeEach { frame -> - if (frame is Frame.Text) { - logger.debug { "Conversion of the Frame: ${frame.readText()}" } - send(Json.decodeFromString(frame.readText())) - } else { - logger.warn { "The frame is not a text frame" } - } - } + val logger = KotlinLogging.logger { } + return produce(capacity = bufferSize) { + frames.consumeEach { frame -> + if (frame is Frame.Text) { + logger.debug { "Conversion of the Frame: ${frame.readText()}" } + send(Json.decodeFromString(frame.readText())) + } else { + logger.warn { "The frame is not a text frame" } + } } + } } inline fun CoroutineScope.fromFrameChannel(frames: SendChannel): SendChannel { - val channel = Channel() - launch { - channel.consumeEach { obj -> - frames.send(Frame.Text(Json.encodeToString(obj))) - } + val channel = Channel() + launch { + channel.consumeEach { obj -> + frames.send(Frame.Text(Json.encodeToString(obj))) } - return channel + } + return channel } diff --git a/src/main/kotlin/eventDemo/libs/command/Command.kt b/src/main/kotlin/eventDemo/libs/command/Command.kt index be78170..fed17d0 100644 --- a/src/main/kotlin/eventDemo/libs/command/Command.kt +++ b/src/main/kotlin/eventDemo/libs/command/Command.kt @@ -10,11 +10,12 @@ import java.util.UUID @JvmInline @Serializable(with = CommandIdSerializer::class) value class CommandId( - private val id: UUID = UUID.randomUUID(), + private val id: UUID = UUID.randomUUID(), ) { - constructor(id: String) : this(UUID.fromString(id)) + constructor(id: String) : this(UUID.fromString(id)) - override fun toString(): String = id.toString() + override fun toString(): String = + id.toString() } /** @@ -23,5 +24,5 @@ value class CommandId( * A command is a request for an action. */ interface Command { - val id: CommandId + val id: CommandId } diff --git a/src/main/kotlin/eventDemo/libs/command/CommandStream.kt b/src/main/kotlin/eventDemo/libs/command/CommandStream.kt index 8f69967..30cb605 100644 --- a/src/main/kotlin/eventDemo/libs/command/CommandStream.kt +++ b/src/main/kotlin/eventDemo/libs/command/CommandStream.kt @@ -10,26 +10,26 @@ import kotlinx.coroutines.launch * The stream contains a list of all actions yet to be executed. */ interface CommandStream { - /** - * A class to implement success/failed action. - */ - interface ComputeStatus { - suspend fun ack() + /** + * A class to implement success/failed action. + */ + interface ComputeStatus { + suspend fun ack() - suspend fun nack() - } - - /** - * Apply an action to all command income in the stream. - */ - suspend fun process(action: CommandBlock) - - @OptIn(DelicateCoroutinesApi::class) - fun blockAndProcess(action: CommandBlock) { - GlobalScope.launch { - process(action) - } + suspend fun nack() + } + + /** + * Apply an action to all command income in the stream. + */ + suspend fun process(action: CommandBlock) + + @OptIn(DelicateCoroutinesApi::class) + fun blockAndProcess(action: CommandBlock) { + GlobalScope.launch { + process(action) } + } } typealias CommandBlock = suspend CommandStream.ComputeStatus.(C) -> Unit diff --git a/src/main/kotlin/eventDemo/libs/command/CommandStreamChannel.kt b/src/main/kotlin/eventDemo/libs/command/CommandStreamChannel.kt index a2f2658..5ccc6f8 100644 --- a/src/main/kotlin/eventDemo/libs/command/CommandStreamChannel.kt +++ b/src/main/kotlin/eventDemo/libs/command/CommandStreamChannel.kt @@ -9,87 +9,88 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes class CommandStreamChannelBuilder( - private val maxCacheTime: Duration = 10.minutes, + private val maxCacheTime: Duration = 10.minutes, ) { - operator fun invoke(incoming: ReceiveChannel): CommandStreamChannel = CommandStreamChannel(incoming, maxCacheTime) + operator fun invoke(incoming: ReceiveChannel): CommandStreamChannel = + CommandStreamChannel(incoming, maxCacheTime) } /** * Manage [Command]'s with kotlin Channel */ class CommandStreamChannel( - private val incoming: ReceiveChannel, - private val maxCacheTime: Duration = 10.minutes, + private val incoming: ReceiveChannel, + private val maxCacheTime: Duration = 10.minutes, ) : CommandStream { - private val logger = KotlinLogging.logger {} - private val executedCommand: ConcurrentHashMap> = ConcurrentHashMap() + private val logger = KotlinLogging.logger {} + private val executedCommand: ConcurrentHashMap> = ConcurrentHashMap() - override suspend fun process(action: CommandBlock) { - for (command in incoming) { - val now = Clock.System.now() - val (status, _) = executedCommand.computeIfAbsent(command.id) { Pair(false, now) } + override suspend fun process(action: CommandBlock) { + for (command in incoming) { + val now = Clock.System.now() + val (status, _) = executedCommand.computeIfAbsent(command.id) { Pair(false, now) } - if (status) { - logger.atWarn { - message = "Command already executed: $command" - payload = mapOf("command" to command) - } - } else { - compute(command, action) - } - executedCommand - .filterValues { (_, date) -> - (date + maxCacheTime) > now - }.keys - .forEach { - executedCommand.remove(it) - } - } - } - - private suspend fun compute( - command: C, - action: CommandBlock, - ) { - val status = - object : CommandStream.ComputeStatus { - var isSet: Boolean = false - - override suspend fun ack() { - if (!isSet) markAsSuccess(command) else error("Already NACK") - isSet = true - } - - override suspend fun nack() { - if (!isSet) markAsFailed(command) else error("Already ACK") - isSet = true - } - } - - val actionResult = runCatching { status.action(command) } - if (actionResult.isFailure) { - logger.atInfo { - message = "Error on compute the Command: $command" - payload = mapOf("command" to command) - cause = actionResult.exceptionOrNull() - } - markAsFailed(command) - } else if (!status.isSet) { - status.ack() - } - } - - private suspend fun markAsSuccess(command: C) { - logger.atInfo { - message = "Compute command SUCCESS: $command" - payload = mapOf("command" to command) - } - } - - private suspend fun markAsFailed(command: C) { + if (status) { logger.atWarn { - message = "Compute command FAILED: $command" - payload = mapOf("command" to command) + message = "Command already executed: $command" + payload = mapOf("command" to command) + } + } else { + compute(command, action) + } + executedCommand + .filterValues { (_, date) -> + (date + maxCacheTime) > now + }.keys + .forEach { + executedCommand.remove(it) } } + } + + private suspend fun compute( + command: C, + action: CommandBlock, + ) { + val status = + object : CommandStream.ComputeStatus { + var isSet: Boolean = false + + override suspend fun ack() { + if (!isSet) markAsSuccess(command) else error("Already NACK") + isSet = true + } + + override suspend fun nack() { + if (!isSet) markAsFailed(command) else error("Already ACK") + isSet = true + } + } + + val actionResult = runCatching { status.action(command) } + if (actionResult.isFailure) { + logger.atInfo { + message = "Error on compute the Command: $command" + payload = mapOf("command" to command) + cause = actionResult.exceptionOrNull() + } + markAsFailed(command) + } else if (!status.isSet) { + status.ack() + } + } + + private suspend fun markAsSuccess(command: C) { + logger.atInfo { + message = "Compute command SUCCESS: $command" + payload = mapOf("command" to command) + } + } + + private suspend fun markAsFailed(command: C) { + logger.atWarn { + message = "Compute command FAILED: $command" + payload = mapOf("command" to command) + } + } } diff --git a/src/main/kotlin/eventDemo/libs/event/Event.kt b/src/main/kotlin/eventDemo/libs/event/Event.kt index 46b5ca2..749dc82 100644 --- a/src/main/kotlin/eventDemo/libs/event/Event.kt +++ b/src/main/kotlin/eventDemo/libs/event/Event.kt @@ -8,7 +8,7 @@ import java.util.UUID * @see Event */ interface AggregateId { - val id: UUID + val id: UUID } /** @@ -16,8 +16,8 @@ interface AggregateId { * @see EventStream */ interface Event { - val eventId: UUID - val aggregateId: ID - val createdAt: Instant - val version: Int + val eventId: UUID + val aggregateId: ID + val createdAt: Instant + val version: Int } diff --git a/src/main/kotlin/eventDemo/libs/event/EventBus.kt b/src/main/kotlin/eventDemo/libs/event/EventBus.kt index c89ceda..dbf87e3 100644 --- a/src/main/kotlin/eventDemo/libs/event/EventBus.kt +++ b/src/main/kotlin/eventDemo/libs/event/EventBus.kt @@ -1,13 +1,13 @@ package eventDemo.libs.event interface EventBus, ID : AggregateId> { - fun publish(event: E) + fun publish(event: E) - /** - * @param priority The higher the priority, the more it will be called first - */ - fun subscribe( - priority: Int = 0, - block: suspend (E) -> Unit, - ) + /** + * @param priority The higher the priority, the more it will be called first + */ + fun subscribe( + priority: Int = 0, + block: suspend (E) -> Unit, + ) } diff --git a/src/main/kotlin/eventDemo/libs/event/EventBusInMemory.kt b/src/main/kotlin/eventDemo/libs/event/EventBusInMemory.kt index 130f3ae..ef62111 100644 --- a/src/main/kotlin/eventDemo/libs/event/EventBusInMemory.kt +++ b/src/main/kotlin/eventDemo/libs/event/EventBusInMemory.kt @@ -3,22 +3,22 @@ package eventDemo.libs.event import kotlinx.coroutines.runBlocking class EventBusInMemory, ID : AggregateId> : EventBus { - private val subscribers: MutableList Unit>> = mutableListOf() + private val subscribers: MutableList Unit>> = mutableListOf() - override fun publish(event: E) { - subscribers - .sortedByDescending { (priority, _) -> priority } - .forEach { (_, block) -> - runBlocking { - block(event) - } - } - } + override fun publish(event: E) { + subscribers + .sortedByDescending { (priority, _) -> priority } + .forEach { (_, block) -> + runBlocking { + block(event) + } + } + } - override fun subscribe( - priority: Int, - block: suspend (E) -> Unit, - ) { - subscribers.add(priority to block) - } + override fun subscribe( + priority: Int, + block: suspend (E) -> Unit, + ) { + subscribers.add(priority to block) + } } diff --git a/src/main/kotlin/eventDemo/libs/event/EventStore.kt b/src/main/kotlin/eventDemo/libs/event/EventStore.kt index 8717332..e27625e 100644 --- a/src/main/kotlin/eventDemo/libs/event/EventStore.kt +++ b/src/main/kotlin/eventDemo/libs/event/EventStore.kt @@ -1,7 +1,7 @@ package eventDemo.libs.event interface EventStore, ID : AggregateId> { - fun getStream(aggregateId: ID): EventStream + fun getStream(aggregateId: ID): EventStream - fun publish(event: E) + fun publish(event: E) } diff --git a/src/main/kotlin/eventDemo/libs/event/EventStoreInMemory.kt b/src/main/kotlin/eventDemo/libs/event/EventStoreInMemory.kt index fd3480a..2e0ec52 100644 --- a/src/main/kotlin/eventDemo/libs/event/EventStoreInMemory.kt +++ b/src/main/kotlin/eventDemo/libs/event/EventStoreInMemory.kt @@ -4,9 +4,11 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap class EventStoreInMemory, ID : AggregateId> : EventStore { - private val streams: ConcurrentMap> = ConcurrentHashMap() + private val streams: ConcurrentMap> = ConcurrentHashMap() - override fun getStream(aggregateId: ID): EventStream = streams.computeIfAbsent(aggregateId) { EventStreamInMemory() } + override fun getStream(aggregateId: ID): EventStream = + streams.computeIfAbsent(aggregateId) { EventStreamInMemory() } - override fun publish(event: E) = getStream(event.aggregateId).publish(event) + override fun publish(event: E) = + getStream(event.aggregateId).publish(event) } diff --git a/src/main/kotlin/eventDemo/libs/event/EventStream.kt b/src/main/kotlin/eventDemo/libs/event/EventStream.kt index 0657bcf..8f8946a 100644 --- a/src/main/kotlin/eventDemo/libs/event/EventStream.kt +++ b/src/main/kotlin/eventDemo/libs/event/EventStream.kt @@ -4,16 +4,16 @@ package eventDemo.libs.event * Interface representing an event stream for publishing and reading domain events */ interface EventStream> { - /** Publishes a single event to the event stream */ - fun publish(event: E) + /** Publishes a single event to the event stream */ + fun publish(event: E) - /** Publishes multiple events to the event stream */ - fun publish(vararg events: E) + /** Publishes multiple events to the event stream */ + fun publish(vararg events: E) - /** Reads all events */ - fun readAll(): Set + /** Reads all events */ + fun readAll(): Set - fun readGreaterOfVersion(version: Int): Set + fun readGreaterOfVersion(version: Int): Set - fun readVersionBetween(version: IntRange): Set + fun readVersionBetween(version: IntRange): Set } diff --git a/src/main/kotlin/eventDemo/libs/event/EventStreamInMemory.kt b/src/main/kotlin/eventDemo/libs/event/EventStreamInMemory.kt index 8d37f20..742f582 100644 --- a/src/main/kotlin/eventDemo/libs/event/EventStreamInMemory.kt +++ b/src/main/kotlin/eventDemo/libs/event/EventStreamInMemory.kt @@ -10,32 +10,33 @@ import java.util.concurrent.ConcurrentLinkedQueue * All methods are implemented. */ class EventStreamInMemory> : EventStream { - private val logger = KotlinLogging.logger {} - private val events: Queue = ConcurrentLinkedQueue() + private val logger = KotlinLogging.logger {} + private val events: Queue = ConcurrentLinkedQueue() - override fun publish(event: E) { - if (events.none { it.eventId == event.eventId }) { - events.add(event) - logger.atInfo { - message = "Event published: $event" - payload = mapOf("event" to event) - } - } + override fun publish(event: E) { + if (events.none { it.eventId == event.eventId }) { + events.add(event) + logger.atInfo { + message = "Event published: $event" + payload = mapOf("event" to event) + } } + } - override fun publish(vararg events: E) { - events.forEach { publish(it) } - } + override fun publish(vararg events: E) { + events.forEach { publish(it) } + } - override fun readAll(): Set = events.toSet() + override fun readAll(): Set = + events.toSet() - override fun readGreaterOfVersion(version: Int): Set = - events - .filter { it.version > version } - .toSet() + override fun readGreaterOfVersion(version: Int): Set = + events + .filter { it.version > version } + .toSet() - override fun readVersionBetween(version: IntRange): Set = - events - .filter { version.contains(it.version) } - .toSet() + override fun readVersionBetween(version: IntRange): Set = + events + .filter { version.contains(it.version) } + .toSet() } diff --git a/src/main/kotlin/eventDemo/libs/event/VersionBuilder.kt b/src/main/kotlin/eventDemo/libs/event/VersionBuilder.kt index 4f3e2df..38ab05e 100644 --- a/src/main/kotlin/eventDemo/libs/event/VersionBuilder.kt +++ b/src/main/kotlin/eventDemo/libs/event/VersionBuilder.kt @@ -1,7 +1,7 @@ package eventDemo.libs.event interface VersionBuilder { - fun buildNextVersion(aggregateId: AggregateId): Int + fun buildNextVersion(aggregateId: AggregateId): Int - fun getLastVersion(aggregateId: AggregateId): Int + fun getLastVersion(aggregateId: AggregateId): Int } diff --git a/src/main/kotlin/eventDemo/libs/event/VersionBuilderLocal.kt b/src/main/kotlin/eventDemo/libs/event/VersionBuilderLocal.kt index 3b5ca07..da39308 100644 --- a/src/main/kotlin/eventDemo/libs/event/VersionBuilderLocal.kt +++ b/src/main/kotlin/eventDemo/libs/event/VersionBuilderLocal.kt @@ -5,17 +5,18 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger class VersionBuilderLocal : VersionBuilder { - private val logger = KotlinLogging.logger { } - private val versions: ConcurrentHashMap = ConcurrentHashMap() + private val logger = KotlinLogging.logger { } + private val versions: ConcurrentHashMap = ConcurrentHashMap() - override fun buildNextVersion(aggregateId: AggregateId): Int = - versionOfAggregate(aggregateId) - .addAndGet(1) - .also { logger.debug { "New version $it" } } + override fun buildNextVersion(aggregateId: AggregateId): Int = + versionOfAggregate(aggregateId) + .addAndGet(1) + .also { logger.debug { "New version $it" } } - override fun getLastVersion(aggregateId: AggregateId): Int = versionOfAggregate(aggregateId).toInt() + override fun getLastVersion(aggregateId: AggregateId): Int = + versionOfAggregate(aggregateId).toInt() - private fun versionOfAggregate(aggregateId: AggregateId) = - versions - .computeIfAbsent(aggregateId) { AtomicInteger(0) } + private fun versionOfAggregate(aggregateId: AggregateId) = + versions + .computeIfAbsent(aggregateId) { AtomicInteger(0) } } diff --git a/src/test/kotlin/eventDemo/Helpers.kt b/src/test/kotlin/eventDemo/Helpers.kt index 4942235..900768d 100644 --- a/src/test/kotlin/eventDemo/Helpers.kt +++ b/src/test/kotlin/eventDemo/Helpers.kt @@ -3,8 +3,10 @@ package eventDemo import eventDemo.app.entity.Card import eventDemo.app.entity.Deck -fun Deck.allCardCount(): Int = stack.size + discard.size + playersHands.values.flatten().size +fun Deck.allCardCount(): Int = + stack.size + discard.size + playersHands.values.flatten().size -fun Deck.allCards(): Set = stack + discard + playersHands.values.flatten() +fun Deck.allCards(): Set = + stack + discard + playersHands.values.flatten() // suspend fun SendChannel.send(command: GameCommand) = send(Frame.Text(Json.encodeToString(command))) diff --git a/src/test/kotlin/eventDemo/app/command/GameCommandHandlerTest.kt b/src/test/kotlin/eventDemo/app/command/GameCommandHandlerTest.kt index d7827eb..c0b6cb5 100644 --- a/src/test/kotlin/eventDemo/app/command/GameCommandHandlerTest.kt +++ b/src/test/kotlin/eventDemo/app/command/GameCommandHandlerTest.kt @@ -20,26 +20,26 @@ import kotlin.test.assertIs @OptIn(DelicateCoroutinesApi::class) class GameCommandHandlerTest : - FunSpec({ - test("handle a command should execute the command") { - koinApplication { modules(appKoinModule) }.koin.apply { - val commandHandler by inject() - val notificationListener by inject() - val gameId = GameId() - val player = Player("Tesla") - val channelCommand = Channel(Channel.BUFFERED) - val channelNotification = Channel(Channel.BUFFERED) - ReactionEventListener(get(), get(), get()).init() - notificationListener.startListening(channelNotification, player) + FunSpec({ + test("handle a command should execute the command") { + koinApplication { modules(appKoinModule) }.koin.apply { + val commandHandler by inject() + val notificationListener by inject() + val gameId = GameId() + val player = Player("Tesla") + val channelCommand = Channel(Channel.BUFFERED) + val channelNotification = Channel(Channel.BUFFERED) + ReactionEventListener(get(), get(), get()).init() + notificationListener.startListening(channelNotification, player) - GlobalScope.launch { - commandHandler.handle(player, channelCommand, channelNotification) - } - - channelCommand.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player))) - assertIs(channelNotification.receive()).let { - it.players shouldContain player - } - } + GlobalScope.launch { + commandHandler.handle(player, channelCommand, channelNotification) } - }) + + channelCommand.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player))) + assertIs(channelNotification.receive()).let { + it.players shouldContain player + } + } + } + }) diff --git a/src/test/kotlin/eventDemo/app/command/GameCommandRunnerTest.kt b/src/test/kotlin/eventDemo/app/command/GameCommandRunnerTest.kt index 0a17068..577f62d 100644 --- a/src/test/kotlin/eventDemo/app/command/GameCommandRunnerTest.kt +++ b/src/test/kotlin/eventDemo/app/command/GameCommandRunnerTest.kt @@ -3,6 +3,6 @@ package eventDemo.app.command import io.kotest.core.spec.style.FunSpec class GameCommandRunnerTest : - FunSpec({ - test("run should run the correct command") { } - }) + FunSpec({ + test("run should run the correct command") { } + }) diff --git a/src/test/kotlin/eventDemo/app/command/command/ICantPlayCommandTest.kt b/src/test/kotlin/eventDemo/app/command/command/ICantPlayCommandTest.kt index 0711aab..6185b1c 100644 --- a/src/test/kotlin/eventDemo/app/command/command/ICantPlayCommandTest.kt +++ b/src/test/kotlin/eventDemo/app/command/command/ICantPlayCommandTest.kt @@ -3,7 +3,7 @@ package eventDemo.app.command.command import io.kotest.core.spec.style.FunSpec class ICantPlayCommandTest : - FunSpec({ + FunSpec({ - xtest("run should publish the event") { } - }) + xtest("run should publish the event") { } + }) diff --git a/src/test/kotlin/eventDemo/app/command/command/IWantToJoinTheGameCommandTest.kt b/src/test/kotlin/eventDemo/app/command/command/IWantToJoinTheGameCommandTest.kt index ecd24ba..0a880db 100644 --- a/src/test/kotlin/eventDemo/app/command/command/IWantToJoinTheGameCommandTest.kt +++ b/src/test/kotlin/eventDemo/app/command/command/IWantToJoinTheGameCommandTest.kt @@ -3,7 +3,7 @@ package eventDemo.app.command.command import io.kotest.core.spec.style.FunSpec class IWantToJoinTheGameCommandTest : - FunSpec({ + FunSpec({ - xtest("run should publish the event") { } - }) + xtest("run should publish the event") { } + }) diff --git a/src/test/kotlin/eventDemo/app/command/command/IWantToPlayCardCommandTest.kt b/src/test/kotlin/eventDemo/app/command/command/IWantToPlayCardCommandTest.kt index e8ffe1d..5111f5d 100644 --- a/src/test/kotlin/eventDemo/app/command/command/IWantToPlayCardCommandTest.kt +++ b/src/test/kotlin/eventDemo/app/command/command/IWantToPlayCardCommandTest.kt @@ -3,7 +3,7 @@ package eventDemo.app.command.command import io.kotest.core.spec.style.FunSpec class IWantToPlayCardCommandTest : - FunSpec({ + FunSpec({ - xtest("run should publish the event") { } - }) + xtest("run should publish the event") { } + }) diff --git a/src/test/kotlin/eventDemo/app/command/command/IamReadyToPlayCommandTest.kt b/src/test/kotlin/eventDemo/app/command/command/IamReadyToPlayCommandTest.kt index 99cbc3e..4fa09fb 100644 --- a/src/test/kotlin/eventDemo/app/command/command/IamReadyToPlayCommandTest.kt +++ b/src/test/kotlin/eventDemo/app/command/command/IamReadyToPlayCommandTest.kt @@ -3,7 +3,7 @@ package eventDemo.app.command.command import io.kotest.core.spec.style.FunSpec class IamReadyToPlayCommandTest : - FunSpec({ + FunSpec({ - xtest("run should publish the event") { } - }) + xtest("run should publish the event") { } + }) diff --git a/src/test/kotlin/eventDemo/app/entity/DeckTest.kt b/src/test/kotlin/eventDemo/app/entity/DeckTest.kt index 1ebb251..bf59e86 100644 --- a/src/test/kotlin/eventDemo/app/entity/DeckTest.kt +++ b/src/test/kotlin/eventDemo/app/entity/DeckTest.kt @@ -8,97 +8,97 @@ import io.kotest.matchers.ints.shouldBeExactly import kotlin.test.assertNotNull class DeckTest : - FunSpec({ - val totalCardsNumber = 104 - test("newWithoutPlayers") { - // When - val deck = Deck.newWithoutPlayers() + FunSpec({ + val totalCardsNumber = 104 + test("newWithoutPlayers") { + // When + val deck = Deck.newWithoutPlayers() - // Then - deck.stack.size shouldBeExactly totalCardsNumber - deck.discard.size shouldBeExactly 0 - deck.playersHands.size shouldBeExactly 0 + // Then + deck.stack.size shouldBeExactly totalCardsNumber + deck.discard.size shouldBeExactly 0 + deck.playersHands.size shouldBeExactly 0 - deck.allCardCount() shouldBeExactly totalCardsNumber - deck.allCards().shouldBeUnique() - deck.allCards().map { it.id }.shouldBeUnique() - } + deck.allCardCount() shouldBeExactly totalCardsNumber + deck.allCards().shouldBeUnique() + deck.allCards().map { it.id }.shouldBeUnique() + } - test("initHands should be generate the hands of all players from the stack") { - // Given - val playerNumbers = 4 - val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() - val deck = Deck.newWithoutPlayers() + test("initHands should be generate the hands of all players from the stack") { + // Given + val playerNumbers = 4 + val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() + val deck = Deck.newWithoutPlayers() - // When - val initDeck = deck.initHands(players) + // When + val initDeck = deck.initHands(players) - // Then - initDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - initDeck.discard.size shouldBeExactly 0 - initDeck.playersHands.size shouldBeExactly playerNumbers - initDeck.playersHands.forEach { (_, cards) -> cards.size shouldBeExactly 7 } - initDeck.allCardCount() shouldBeExactly totalCardsNumber - } + // Then + initDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) + initDeck.discard.size shouldBeExactly 0 + initDeck.playersHands.size shouldBeExactly playerNumbers + initDeck.playersHands.forEach { (_, cards) -> cards.size shouldBeExactly 7 } + initDeck.allCardCount() shouldBeExactly totalCardsNumber + } - test("takeOneCardFromStackTo player") { - // Given - val playerNumbers = 4 - val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() - val deck = Deck.newWithoutPlayers().initHands(players) - val firstPlayer = players.first() + test("takeOneCardFromStackTo player") { + // Given + val playerNumbers = 4 + val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() + val deck = Deck.newWithoutPlayers().initHands(players) + val firstPlayer = players.first() - // When - val modifiedDeck = deck.takeOneCardFromStackTo(firstPlayer) + // When + val modifiedDeck = deck.takeOneCardFromStackTo(firstPlayer) - // Then - modifiedDeck.discard.size shouldBeExactly 0 - modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - 1 - modifiedDeck.playersHands.size shouldBeExactly playerNumbers - assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 7 + 1 - modifiedDeck.playersHands - .filterKeys { it != firstPlayer.id } - .forEach { (_, cards) -> cards.size shouldBeExactly 7 } - modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber - } + // Then + modifiedDeck.discard.size shouldBeExactly 0 + modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - 1 + modifiedDeck.playersHands.size shouldBeExactly playerNumbers + assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 7 + 1 + modifiedDeck.playersHands + .filterKeys { it != firstPlayer.id } + .forEach { (_, cards) -> cards.size shouldBeExactly 7 } + modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber + } - test("putOneCardFromHand") { - // Given - val playerNumbers = 4 - val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() - val deck = Deck.newWithoutPlayers().initHands(players) - val firstPlayer = players.first() + test("putOneCardFromHand") { + // Given + val playerNumbers = 4 + val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() + val deck = Deck.newWithoutPlayers().initHands(players) + val firstPlayer = players.first() - // When - val card = deck.playersHands.getHand(firstPlayer)!!.first() - val modifiedDeck = deck.putOneCardFromHand(firstPlayer, card) + // When + val card = deck.playersHands.getHand(firstPlayer)!!.first() + val modifiedDeck = deck.putOneCardFromHand(firstPlayer, card) - // Then - modifiedDeck.discard.size shouldBeExactly 1 - modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - modifiedDeck.playersHands.size shouldBeExactly playerNumbers - assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 6 - modifiedDeck.playersHands - .filterKeys { it != firstPlayer.id } - .forEach { (_, cards) -> cards.size shouldBeExactly 7 } - modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber - } + // Then + modifiedDeck.discard.size shouldBeExactly 1 + modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) + modifiedDeck.playersHands.size shouldBeExactly playerNumbers + assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 6 + modifiedDeck.playersHands + .filterKeys { it != firstPlayer.id } + .forEach { (_, cards) -> cards.size shouldBeExactly 7 } + modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber + } - test("placeFirstCardOnDiscard") { - // Given - val playerNumbers = 4 - val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() - val deck = Deck.newWithoutPlayers().initHands(players) + test("placeFirstCardOnDiscard") { + // Given + val playerNumbers = 4 + val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() + val deck = Deck.newWithoutPlayers().initHands(players) - // When - val modifiedDeck = deck.placeFirstCardOnDiscard() + // When + val modifiedDeck = deck.placeFirstCardOnDiscard() - // Then - modifiedDeck.discard.size shouldBeExactly 1 - modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - 1 - modifiedDeck.playersHands.size shouldBeExactly playerNumbers - modifiedDeck.playersHands - .forEach { (_, cards) -> cards.size shouldBeExactly 7 } - modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber - } - }) + // Then + modifiedDeck.discard.size shouldBeExactly 1 + modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - 1 + modifiedDeck.playersHands.size shouldBeExactly playerNumbers + modifiedDeck.playersHands + .forEach { (_, cards) -> cards.size shouldBeExactly 7 } + modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber + } + }) diff --git a/src/test/kotlin/eventDemo/app/entity/PlayerHandKtTest.kt b/src/test/kotlin/eventDemo/app/entity/PlayerHandKtTest.kt index 1111596..0c84050 100644 --- a/src/test/kotlin/eventDemo/app/entity/PlayerHandKtTest.kt +++ b/src/test/kotlin/eventDemo/app/entity/PlayerHandKtTest.kt @@ -5,37 +5,37 @@ import io.kotest.matchers.ints.shouldBeExactly import kotlin.test.assertNotNull class PlayerHandKtTest : - FunSpec({ - test("addCards") { - // Given - val playerNumbers = 4 - val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() - val firstPlayer = players.first() - val playersHands = PlayersHands(players) - val card = Card.NumericCard(0, Card.Color.Red) + FunSpec({ + test("addCards") { + // Given + val playerNumbers = 4 + val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() + val firstPlayer = players.first() + val playersHands = PlayersHands(players) + val card = Card.NumericCard(0, Card.Color.Red) - // When - val newHands: PlayersHands = playersHands.addCards(firstPlayer, listOf(card)) + // When + val newHands: PlayersHands = playersHands.addCards(firstPlayer, listOf(card)) - assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1 - assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0 - } + assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1 + assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0 + } - test("removeCard") { - // Given - val playerNumbers = 4 - val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() - val firstPlayer = players.first() - val card1 = Card.NumericCard(1, Card.Color.Red) - val card2 = Card.NumericCard(2, Card.Color.Red) - val playersHands: PlayersHands = - PlayersHands(players) - .addCards(firstPlayer, listOf(card1, card2)) + test("removeCard") { + // Given + val playerNumbers = 4 + val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() + val firstPlayer = players.first() + val card1 = Card.NumericCard(1, Card.Color.Red) + val card2 = Card.NumericCard(2, Card.Color.Red) + val playersHands: PlayersHands = + PlayersHands(players) + .addCards(firstPlayer, listOf(card1, card2)) - // When - val newHands: PlayersHands = playersHands.removeCard(firstPlayer, card1) + // When + val newHands: PlayersHands = playersHands.removeCard(firstPlayer, card1) - assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1 - assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0 - } - }) + assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1 + assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0 + } + }) diff --git a/src/test/kotlin/eventDemo/app/entity/PlayersHandsTest.kt b/src/test/kotlin/eventDemo/app/entity/PlayersHandsTest.kt index 72aed5a..5c57990 100644 --- a/src/test/kotlin/eventDemo/app/entity/PlayersHandsTest.kt +++ b/src/test/kotlin/eventDemo/app/entity/PlayersHandsTest.kt @@ -3,13 +3,13 @@ package eventDemo.app.entity import io.kotest.core.spec.style.FunSpec class PlayersHandsTest : - FunSpec({ + FunSpec({ - xtest("getHand should return the hand of the player") { } + xtest("getHand should return the hand of the player") { } - xtest("removeCard should remove the card") { } + xtest("removeCard should remove the card") { } - xtest("addCard should add the card to the correct hand") { } + xtest("addCard should add the card to the correct hand") { } - xtest("toPlayersHands should build object from map") { } - }) + xtest("toPlayersHands should build object from map") { } + }) diff --git a/src/test/kotlin/eventDemo/app/event/GameEventHandlerTest.kt b/src/test/kotlin/eventDemo/app/event/GameEventHandlerTest.kt index 0705ecd..e5129ee 100644 --- a/src/test/kotlin/eventDemo/app/event/GameEventHandlerTest.kt +++ b/src/test/kotlin/eventDemo/app/event/GameEventHandlerTest.kt @@ -3,8 +3,8 @@ package eventDemo.app.event import io.kotest.core.spec.style.FunSpec class GameEventHandlerTest : - FunSpec({ - xtest("handle event should publish the event to the stream") { } - xtest("handle event should build the registered projection") { } - xtest("handle event should publish the event to the bus") { } - }) + FunSpec({ + xtest("handle event should publish the event to the stream") { } + xtest("handle event should build the registered projection") { } + xtest("handle event should publish the event to the bus") { } + }) diff --git a/src/test/kotlin/eventDemo/app/event/projection/GameStateBuilderTest.kt b/src/test/kotlin/eventDemo/app/event/projection/GameStateBuilderTest.kt index 3188156..633993b 100644 --- a/src/test/kotlin/eventDemo/app/event/projection/GameStateBuilderTest.kt +++ b/src/test/kotlin/eventDemo/app/event/projection/GameStateBuilderTest.kt @@ -15,112 +15,112 @@ import kotlin.test.assertIs import kotlin.test.assertNotNull class GameStateBuilderTest : - FunSpec({ - test("apply") { - disableShuffleDeck() - val versionBuilder = VersionBuilderLocal() - val gameId = GameId() - val player1 = Player(name = "Nikola") - val player2 = Player(name = "Einstein") + FunSpec({ + test("apply") { + disableShuffleDeck() + val versionBuilder = VersionBuilderLocal() + val gameId = GameId() + val player1 = Player(name = "Nikola") + val player2 = Player(name = "Einstein") - GameState(gameId) - .run { - val event = - NewPlayerEvent( - aggregateId = gameId, - player = player1, - version = versionBuilder.buildNextVersion(gameId), - ) - apply(event).also { state -> - state.aggregateId shouldBeEqual gameId - state.isReady shouldBeEqual false - state.isStarted shouldBeEqual false - } - }.run { - val event = - NewPlayerEvent( - aggregateId = gameId, - player = player2, - version = versionBuilder.buildNextVersion(gameId), - ) - apply(event).also { state -> - state.aggregateId shouldBeEqual gameId - state.players shouldBeEqual setOf(player1, player2) - } - }.run { - val event = - PlayerReadyEvent( - aggregateId = gameId, - player = player1, - version = versionBuilder.buildNextVersion(gameId), - ) - apply(event).also { state -> - state.aggregateId shouldBeEqual gameId - state.readyPlayers shouldBeEqual setOf(player1) - } - }.run { - val event = - PlayerReadyEvent( - aggregateId = gameId, - player = player2, - version = versionBuilder.buildNextVersion(gameId), - ) - apply(event).also { state -> - state.aggregateId shouldBeEqual gameId - state.readyPlayers shouldBeEqual setOf(player1, player2) - state.isReady shouldBeEqual true - state.isStarted shouldBeEqual false - } - }.run { - val event = - GameStartedEvent.new( - id = gameId, - players = setOf(player1, player2), - shuffleIsDisabled = true, - version = versionBuilder.buildNextVersion(gameId), - ) - apply(event).also { state -> - state.aggregateId shouldBeEqual gameId - state.isStarted shouldBeEqual true - assertIs(state.deck.stack.first()).let { - it.number shouldBeEqual 6 - it.color shouldBeEqual Card.Color.Red - } - } - }.run { - val playedCard = playableCards(player1)[0] - val event = - CardIsPlayedEvent( - aggregateId = gameId, - card = playedCard, - player = player1, - version = versionBuilder.buildNextVersion(gameId), - ) - apply(event).also { state -> - state.aggregateId shouldBeEqual gameId - assertNotNull(state.cardOnCurrentStack).card shouldBeEqual playedCard - assertIs(playedCard).let { - it.number shouldBeEqual 0 - it.color shouldBeEqual Card.Color.Red - } - } - }.run { - val playedCard = playableCards(player2)[0] - val event = - CardIsPlayedEvent( - aggregateId = gameId, - card = playedCard, - player = player2, - version = versionBuilder.buildNextVersion(gameId), - ) - apply(event).also { state -> - state.aggregateId shouldBeEqual gameId - assertNotNull(state.cardOnCurrentStack).card shouldBeEqual playedCard - assertIs(playedCard).let { - it.number shouldBeEqual 7 - it.color shouldBeEqual Card.Color.Red - } - } - } + GameState(gameId) + .run { + val event = + NewPlayerEvent( + aggregateId = gameId, + player = player1, + version = versionBuilder.buildNextVersion(gameId), + ) + apply(event).also { state -> + state.aggregateId shouldBeEqual gameId + state.isReady shouldBeEqual false + state.isStarted shouldBeEqual false + } + }.run { + val event = + NewPlayerEvent( + aggregateId = gameId, + player = player2, + version = versionBuilder.buildNextVersion(gameId), + ) + apply(event).also { state -> + state.aggregateId shouldBeEqual gameId + state.players shouldBeEqual setOf(player1, player2) + } + }.run { + val event = + PlayerReadyEvent( + aggregateId = gameId, + player = player1, + version = versionBuilder.buildNextVersion(gameId), + ) + apply(event).also { state -> + state.aggregateId shouldBeEqual gameId + state.readyPlayers shouldBeEqual setOf(player1) + } + }.run { + val event = + PlayerReadyEvent( + aggregateId = gameId, + player = player2, + version = versionBuilder.buildNextVersion(gameId), + ) + apply(event).also { state -> + state.aggregateId shouldBeEqual gameId + state.readyPlayers shouldBeEqual setOf(player1, player2) + state.isReady shouldBeEqual true + state.isStarted shouldBeEqual false + } + }.run { + val event = + GameStartedEvent.new( + id = gameId, + players = setOf(player1, player2), + shuffleIsDisabled = true, + version = versionBuilder.buildNextVersion(gameId), + ) + apply(event).also { state -> + state.aggregateId shouldBeEqual gameId + state.isStarted shouldBeEqual true + assertIs(state.deck.stack.first()).let { + it.number shouldBeEqual 6 + it.color shouldBeEqual Card.Color.Red + } + } + }.run { + val playedCard = playableCards(player1)[0] + val event = + CardIsPlayedEvent( + aggregateId = gameId, + card = playedCard, + player = player1, + version = versionBuilder.buildNextVersion(gameId), + ) + apply(event).also { state -> + state.aggregateId shouldBeEqual gameId + assertNotNull(state.cardOnCurrentStack).card shouldBeEqual playedCard + assertIs(playedCard).let { + it.number shouldBeEqual 0 + it.color shouldBeEqual Card.Color.Red + } + } + }.run { + val playedCard = playableCards(player2)[0] + val event = + CardIsPlayedEvent( + aggregateId = gameId, + card = playedCard, + player = player2, + version = versionBuilder.buildNextVersion(gameId), + ) + apply(event).also { state -> + state.aggregateId shouldBeEqual gameId + assertNotNull(state.cardOnCurrentStack).card shouldBeEqual playedCard + assertIs(playedCard).let { + it.number shouldBeEqual 7 + it.color shouldBeEqual Card.Color.Red + } + } } - }) + } + }) diff --git a/src/test/kotlin/eventDemo/app/event/projection/GameStateRepositoryTest.kt b/src/test/kotlin/eventDemo/app/event/projection/GameStateRepositoryTest.kt index cbb965a..4829bf5 100644 --- a/src/test/kotlin/eventDemo/app/event/projection/GameStateRepositoryTest.kt +++ b/src/test/kotlin/eventDemo/app/event/projection/GameStateRepositoryTest.kt @@ -18,113 +18,113 @@ import kotlin.test.assertNotNull @OptIn(DelicateCoroutinesApi::class) class GameStateRepositoryTest : - FunSpec({ - val player1 = Player("Tesla") - val player2 = Player(name = "Einstein") + FunSpec({ + val player1 = Player("Tesla") + val player2 = Player(name = "Einstein") - test("GameStateRepository should build the projection when a new event occurs") { - val aggregateId = GameId() - koinApplication { modules(appKoinModule) }.koin.apply { - val repo = get() - val eventHandler = get() - eventHandler - .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) } - .also { event -> - assertNotNull(repo.getUntil(event)).also { - assertNotNull(it.players) shouldBeEqual setOf(player1) - } - assertNotNull(repo.getLast(aggregateId)).also { - assertNotNull(it.players) shouldBeEqual setOf(player1) - } - } + test("GameStateRepository should build the projection when a new event occurs") { + val aggregateId = GameId() + koinApplication { modules(appKoinModule) }.koin.apply { + val repo = get() + val eventHandler = get() + eventHandler + .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) } + .also { event -> + assertNotNull(repo.getUntil(event)).also { + assertNotNull(it.players) shouldBeEqual setOf(player1) } - stopKoin() - } - - test("get should build the last version of the state") { - val aggregateId = GameId() - koinApplication { modules(appKoinModule) }.koin.apply { - val repo = get() - val eventHandler = get() - - eventHandler - .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) } - .also { - assertNotNull(repo.getLast(aggregateId)).also { - assertNotNull(it.players) shouldBeEqual setOf(player1) - } - } - - eventHandler - .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) } - .also { - assertNotNull(repo.getLast(aggregateId)).also { - assertNotNull(it.players) shouldBeEqual setOf(player1, player2) - } - } + assertNotNull(repo.getLast(aggregateId)).also { + assertNotNull(it.players) shouldBeEqual setOf(player1) } - } + } + } + stopKoin() + } - test("getUntil should build the state until the event") { - repeat(10) { - val aggregateId = GameId() - koinApplication { modules(appKoinModule) }.koin.apply { - val repo = get() - val eventHandler = get() + test("get should build the last version of the state") { + val aggregateId = GameId() + koinApplication { modules(appKoinModule) }.koin.apply { + val repo = get() + val eventHandler = get() - val event1 = - eventHandler - .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) } - .also { event1 -> - assertNotNull(repo.getUntil(event1)).also { - assertNotNull(it.players) shouldBeEqual setOf(player1) - } - } + eventHandler + .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) } + .also { + assertNotNull(repo.getLast(aggregateId)).also { + assertNotNull(it.players) shouldBeEqual setOf(player1) + } + } - eventHandler - .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) } - .also { event2 -> - assertNotNull(repo.getUntil(event2)).also { - assertNotNull(it.players) shouldBeEqual setOf(player1, player2) - } - assertNotNull(repo.getUntil(event1)).also { - assertNotNull(it.players) shouldBeEqual setOf(player1) - } - } + eventHandler + .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) } + .also { + assertNotNull(repo.getLast(aggregateId)).also { + assertNotNull(it.players) shouldBeEqual setOf(player1, player2) + } + } + } + } + + test("getUntil should build the state until the event") { + repeat(10) { + val aggregateId = GameId() + koinApplication { modules(appKoinModule) }.koin.apply { + val repo = get() + val eventHandler = get() + + val event1 = + eventHandler + .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) } + .also { event1 -> + assertNotNull(repo.getUntil(event1)).also { + assertNotNull(it.players) shouldBeEqual setOf(player1) } + } + + eventHandler + .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) } + .also { event2 -> + assertNotNull(repo.getUntil(event2)).also { + assertNotNull(it.players) shouldBeEqual setOf(player1, player2) + } + assertNotNull(repo.getUntil(event1)).also { + assertNotNull(it.players) shouldBeEqual setOf(player1) + } } } + } + } - test("getUntil should be concurrently secure") { - val aggregateId = GameId() - koinApplication { modules(appKoinModule) }.koin.apply { - val repo = get() - val eventHandler = get() + test("getUntil should be concurrently secure") { + val aggregateId = GameId() + koinApplication { modules(appKoinModule) }.koin.apply { + val repo = get() + val eventHandler = get() - (1..10) - .map { r -> - GlobalScope - .launch { - repeat(100) { r2 -> - val playerX = Player("player$r$r2") - eventHandler - .handle(aggregateId) { - NewPlayerEvent( - aggregateId = aggregateId, - player = playerX, - version = it, - ) - } - } - } - }.joinAll() - - repo.getLast(aggregateId).run { - lastEventVersion shouldBeEqual 1000 - players shouldHaveSize 1000 + (1..10) + .map { r -> + GlobalScope + .launch { + repeat(100) { r2 -> + val playerX = Player("player$r$r2") + eventHandler + .handle(aggregateId) { + NewPlayerEvent( + aggregateId = aggregateId, + player = playerX, + version = it, + ) + } } - } - } + } + }.joinAll() - xtest("get should be concurrently secure") { } - }) + repo.getLast(aggregateId).run { + lastEventVersion shouldBeEqual 1000 + players shouldHaveSize 1000 + } + } + } + + xtest("get should be concurrently secure") { } + }) diff --git a/src/test/kotlin/eventDemo/app/event/projection/GameStateTest.kt b/src/test/kotlin/eventDemo/app/event/projection/GameStateTest.kt index 1738ad9..78545a8 100644 --- a/src/test/kotlin/eventDemo/app/event/projection/GameStateTest.kt +++ b/src/test/kotlin/eventDemo/app/event/projection/GameStateTest.kt @@ -3,13 +3,13 @@ package eventDemo.app.event.projection import io.kotest.core.spec.style.FunSpec class GameStateTest : - FunSpec({ - xtest("isReady") { } - xtest("nextPlayer") { } - xtest("nextPlayerTurn") { } - xtest("playerDiffIndex") { } - xtest("cardOnBoardIsForYou") { } - xtest("playableCards") { } - xtest("playerHasNoCardLeft") { } - xtest("canBePlayThisCard") { } - }) + FunSpec({ + xtest("isReady") { } + xtest("nextPlayer") { } + xtest("nextPlayerTurn") { } + xtest("playerDiffIndex") { } + xtest("cardOnBoardIsForYou") { } + xtest("playableCards") { } + xtest("playerHasNoCardLeft") { } + xtest("canBePlayThisCard") { } + }) diff --git a/src/test/kotlin/eventDemo/app/event/projection/ProjectionSnapshotRepositoryInMemoryTest.kt b/src/test/kotlin/eventDemo/app/event/projection/ProjectionSnapshotRepositoryInMemoryTest.kt index 2c9ff61..5d11717 100644 --- a/src/test/kotlin/eventDemo/app/event/projection/ProjectionSnapshotRepositoryInMemoryTest.kt +++ b/src/test/kotlin/eventDemo/app/event/projection/ProjectionSnapshotRepositoryInMemoryTest.kt @@ -20,151 +20,151 @@ import kotlin.test.assertNotNull @OptIn(DelicateCoroutinesApi::class) class ProjectionSnapshotRepositoryInMemoryTest : - FunSpec({ + FunSpec({ - test("when call applyAndPutToCache, the getUntil method must be use the built projection cache") { - val eventStore: EventStore = EventStoreInMemory() - val repo = getSnapshotRepoTest(eventStore) - val aggregateId = IdTest() + test("when call applyAndPutToCache, the getUntil method must be use the built projection cache") { + val eventStore: EventStore = EventStoreInMemory() + val repo = getSnapshotRepoTest(eventStore) + val aggregateId = IdTest() - val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = IdTest()) - eventStore.publish(eventOther) - repo.applyAndPutToCache(eventOther) - assertNotNull(repo.getUntil(eventOther)).also { - assertNotNull(it.value) shouldBeEqual "valOther" - } + val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = IdTest()) + eventStore.publish(eventOther) + repo.applyAndPutToCache(eventOther) + assertNotNull(repo.getUntil(eventOther)).also { + assertNotNull(it.value) shouldBeEqual "valOther" + } - val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId) - eventStore.publish(event1) - repo.applyAndPutToCache(event1) - assertNotNull(repo.getLast(event1.aggregateId)).also { - assertNotNull(it.value) shouldBeEqual "val1" - } - assertNotNull(repo.getUntil(event1)).also { - assertNotNull(it.value) shouldBeEqual "val1" - } + val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId) + eventStore.publish(event1) + repo.applyAndPutToCache(event1) + assertNotNull(repo.getLast(event1.aggregateId)).also { + assertNotNull(it.value) shouldBeEqual "val1" + } + assertNotNull(repo.getUntil(event1)).also { + assertNotNull(it.value) shouldBeEqual "val1" + } - val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId) - eventStore.publish(event2) - repo.applyAndPutToCache(event2) - assertNotNull(repo.getLast(event2.aggregateId)).also { - assertNotNull(it.value) shouldBeEqual "val1val2" - } - assertNotNull(repo.getUntil(event1)).also { - assertNotNull(it.value) shouldBeEqual "val1" - } - assertNotNull(repo.getUntil(event2)).also { - assertNotNull(it.value) shouldBeEqual "val1val2" - } - } + val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId) + eventStore.publish(event2) + repo.applyAndPutToCache(event2) + assertNotNull(repo.getLast(event2.aggregateId)).also { + assertNotNull(it.value) shouldBeEqual "val1val2" + } + assertNotNull(repo.getUntil(event1)).also { + assertNotNull(it.value) shouldBeEqual "val1" + } + assertNotNull(repo.getUntil(event2)).also { + assertNotNull(it.value) shouldBeEqual "val1val2" + } + } - test("ProjectionSnapshotRepositoryInMemory should be thread safe") { - val eventStore: EventStore = EventStoreInMemory() - val repo = getSnapshotRepoTest(eventStore) - val aggregateId = IdTest() - val versionBuilder = VersionBuilderLocal() - val lock = ReentrantLock() - (0..9) - .map { - GlobalScope.launch { - (1..10).map { - val eventX = - lock.withLock { - EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId) - .also { eventStore.publish(it) } - } - repo.applyAndPutToCache(eventX) - } - } - }.joinAll() - assertNotNull(repo.getLast(aggregateId)).num shouldBeEqual 100 - } - - test("removeOldSnapshot") { - val versionBuilder = VersionBuilderLocal() - val eventStore: EventStore = EventStoreInMemory() - val repo = getSnapshotRepoTest(eventStore, SnapshotConfig(2)) - val aggregateId = IdTest() - - fun buildEndSendEventX() { - EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId) + test("ProjectionSnapshotRepositoryInMemory should be thread safe") { + val eventStore: EventStore = EventStoreInMemory() + val repo = getSnapshotRepoTest(eventStore) + val aggregateId = IdTest() + val versionBuilder = VersionBuilderLocal() + val lock = ReentrantLock() + (0..9) + .map { + GlobalScope.launch { + (1..10).map { + val eventX = + lock.withLock { + EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId) .also { eventStore.publish(it) } - .also { repo.applyAndPutToCache(it) } + } + repo.applyAndPutToCache(eventX) } + } + }.joinAll() + assertNotNull(repo.getLast(aggregateId)).num shouldBeEqual 100 + } - buildEndSendEventX() - repo.getLast(aggregateId).num shouldBeEqual 1 - buildEndSendEventX() - repo.getLast(aggregateId).num shouldBeEqual 2 - buildEndSendEventX() - repo.getLast(aggregateId).num shouldBeEqual 3 - buildEndSendEventX() - repo.getLast(aggregateId).num shouldBeEqual 4 - } - }) + test("removeOldSnapshot") { + val versionBuilder = VersionBuilderLocal() + val eventStore: EventStore = EventStoreInMemory() + val repo = getSnapshotRepoTest(eventStore, SnapshotConfig(2)) + val aggregateId = IdTest() + + fun buildEndSendEventX() { + EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId) + .also { eventStore.publish(it) } + .also { repo.applyAndPutToCache(it) } + } + + buildEndSendEventX() + repo.getLast(aggregateId).num shouldBeEqual 1 + buildEndSendEventX() + repo.getLast(aggregateId).num shouldBeEqual 2 + buildEndSendEventX() + repo.getLast(aggregateId).num shouldBeEqual 3 + buildEndSendEventX() + repo.getLast(aggregateId).num shouldBeEqual 4 + } + }) @JvmInline private value class IdTest( - override val id: UUID = UUID.randomUUID(), + override val id: UUID = UUID.randomUUID(), ) : AggregateId private data class ProjectionTest( - override val aggregateId: IdTest, - override val lastEventVersion: Int = 0, - var value: String? = null, - var num: Int = 0, + override val aggregateId: IdTest, + override val lastEventVersion: Int = 0, + var value: String? = null, + var num: Int = 0, ) : Projection private sealed interface TestEvents : Event private data class Event1Test( - override val eventId: UUID = UUID.randomUUID(), - override val aggregateId: IdTest, - override val createdAt: Instant = Clock.System.now(), - override val version: Int, - val value1: String, + override val eventId: UUID = UUID.randomUUID(), + override val aggregateId: IdTest, + override val createdAt: Instant = Clock.System.now(), + override val version: Int, + val value1: String, ) : TestEvents private data class Event2Test( - override val eventId: UUID = UUID.randomUUID(), - override val aggregateId: IdTest, - override val createdAt: Instant = Clock.System.now(), - override val version: Int, - val value2: String, + override val eventId: UUID = UUID.randomUUID(), + override val aggregateId: IdTest, + override val createdAt: Instant = Clock.System.now(), + override val version: Int, + val value2: String, ) : TestEvents private data class EventXTest( - override val eventId: UUID = UUID.randomUUID(), - override val aggregateId: IdTest, - override val createdAt: Instant = Clock.System.now(), - override val version: Int, - val num: Int, + override val eventId: UUID = UUID.randomUUID(), + override val aggregateId: IdTest, + override val createdAt: Instant = Clock.System.now(), + override val version: Int, + val num: Int, ) : TestEvents private fun getSnapshotRepoTest( - eventStore: EventStore, - snapshotConfig: SnapshotConfig = SnapshotConfig(2000), + eventStore: EventStore, + snapshotConfig: SnapshotConfig = SnapshotConfig(2000), ): ProjectionSnapshotRepositoryInMemory = - ProjectionSnapshotRepositoryInMemory( - eventStore = eventStore, - initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) }, - snapshotCacheConfig = snapshotConfig, - ) { event -> - this.let { projection -> - when (event) { - is Event1Test -> { - projection.copy(value = (projection.value ?: "") + event.value1) - } - - is Event2Test -> { - projection.copy(value = (projection.value ?: "") + event.value2) - } - - is EventXTest -> { - projection.copy(num = projection.num + event.num) - } - }.copy( - lastEventVersion = event.version, - ) + ProjectionSnapshotRepositoryInMemory( + eventStore = eventStore, + initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) }, + snapshotCacheConfig = snapshotConfig, + ) { event -> + this.let { projection -> + when (event) { + is Event1Test -> { + projection.copy(value = (projection.value ?: "") + event.value1) } + + is Event2Test -> { + projection.copy(value = (projection.value ?: "") + event.value2) + } + + is EventXTest -> { + projection.copy(num = projection.num + event.num) + } + }.copy( + lastEventVersion = event.version, + ) } + } diff --git a/src/test/kotlin/eventDemo/app/query/GameStateRouteTest.kt b/src/test/kotlin/eventDemo/app/query/GameStateRouteTest.kt index ba19bc6..2619d3a 100644 --- a/src/test/kotlin/eventDemo/app/query/GameStateRouteTest.kt +++ b/src/test/kotlin/eventDemo/app/query/GameStateRouteTest.kt @@ -33,89 +33,89 @@ import kotlin.test.assertIs import kotlin.test.assertNotNull class GameStateRouteTest : - FunSpec({ - test("/game/{id}/state on empty game") { - testApplication { - val id = GameId() - val player1 = Player(name = "Nikola") - application { - stopKoin() - configure() - } - - httpClient() - .get("/game/$id/state") { - withAuth(player1) - accept(ContentType.Application.Json) - }.apply { - assertEquals(HttpStatusCode.OK, status, message = bodyAsText()) - val state = call.body() - id shouldBeEqual state.aggregateId - state.players shouldHaveSize 0 - state.isStarted shouldBeEqual false - } - } + FunSpec({ + test("/game/{id}/state on empty game") { + testApplication { + val id = GameId() + val player1 = Player(name = "Nikola") + application { + stopKoin() + configure() } - test("/game/{id}/card/last") { - testApplication { - val gameId = GameId() - val player1 = Player(name = "Nikola") - val player2 = Player(name = "Einstein") - var lastPlayedCard: Card? = null + httpClient() + .get("/game/$id/state") { + withAuth(player1) + accept(ContentType.Application.Json) + }.apply { + assertEquals(HttpStatusCode.OK, status, message = bodyAsText()) + val state = call.body() + id shouldBeEqual state.aggregateId + state.players shouldHaveSize 0 + state.isStarted shouldBeEqual false + } + } + } - application { - stopKoin() - configure() + test("/game/{id}/card/last") { + testApplication { + val gameId = GameId() + val player1 = Player(name = "Nikola") + val player2 = Player(name = "Einstein") + var lastPlayedCard: Card? = null - val eventHandler by inject() - val stateRepo by inject() - runBlocking { - eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) } - eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) } - eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) } - eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) } - eventHandler.handle(gameId) { - GameStartedEvent.new( - gameId, - setOf(player1, player2), - shuffleIsDisabled = true, - it, - ) - } - delay(100) - lastPlayedCard = stateRepo.getLast(gameId).playableCards(player1).first() - assertNotNull(lastPlayedCard) - .let { assertIs(lastPlayedCard) } - .let { - it.number shouldBeEqual 0 - it.color shouldBeEqual Card.Color.Red - } - delay(100) - eventHandler.handle(gameId) { - CardIsPlayedEvent( - gameId, - assertNotNull(lastPlayedCard), - player1, - it, - ) - } - delay(100) - } - } + application { + stopKoin() + configure() - httpClient() - .get("/game/$gameId/card/last") { - withAuth(player1) - accept(ContentType.Application.Json) - }.apply { - assertEquals(HttpStatusCode.OK, status, message = bodyAsText()) - assertEquals(assertNotNull(lastPlayedCard), call.body()) - } + val eventHandler by inject() + val stateRepo by inject() + runBlocking { + eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) } + eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) } + eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) } + eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) } + eventHandler.handle(gameId) { + GameStartedEvent.new( + gameId, + setOf(player1, player2), + shuffleIsDisabled = true, + it, + ) } + delay(100) + lastPlayedCard = stateRepo.getLast(gameId).playableCards(player1).first() + assertNotNull(lastPlayedCard) + .let { assertIs(lastPlayedCard) } + .let { + it.number shouldBeEqual 0 + it.color shouldBeEqual Card.Color.Red + } + delay(100) + eventHandler.handle(gameId) { + CardIsPlayedEvent( + gameId, + assertNotNull(lastPlayedCard), + player1, + it, + ) + } + delay(100) + } } - }) + + httpClient() + .get("/game/$gameId/card/last") { + withAuth(player1) + accept(ContentType.Application.Json) + }.apply { + assertEquals(HttpStatusCode.OK, status, message = bodyAsText()) + assertEquals(assertNotNull(lastPlayedCard), call.body()) + } + } + } + }) private fun HttpRequestBuilder.withAuth(player: Player) { - header("Authorization", "Bearer ${player.makeJwt()}") + header("Authorization", "Bearer ${player.makeJwt()}") } diff --git a/src/test/kotlin/eventDemo/app/query/GameStateTest.kt b/src/test/kotlin/eventDemo/app/query/GameStateTest.kt index b0bfb1c..2bed51c 100644 --- a/src/test/kotlin/eventDemo/app/query/GameStateTest.kt +++ b/src/test/kotlin/eventDemo/app/query/GameStateTest.kt @@ -42,127 +42,127 @@ import kotlin.time.Duration.Companion.seconds @DelicateCoroutinesApi class GameStateTest : - FunSpec({ - test("Simulation of a game") { - withTimeout(2.seconds) { - disableShuffleDeck() - val id = GameId() - val player1 = Player(name = "Nikola") - val player2 = Player(name = "Einstein") - val channelCommand1 = Channel(Channel.BUFFERED) - val channelCommand2 = Channel(Channel.BUFFERED) - val channelNotification1 = Channel(Channel.BUFFERED) - val channelNotification2 = Channel(Channel.BUFFERED) + FunSpec({ + test("Simulation of a game") { + withTimeout(2.seconds) { + disableShuffleDeck() + val id = GameId() + val player1 = Player(name = "Nikola") + val player2 = Player(name = "Einstein") + val channelCommand1 = Channel(Channel.BUFFERED) + val channelCommand2 = Channel(Channel.BUFFERED) + val channelNotification1 = Channel(Channel.BUFFERED) + val channelNotification2 = Channel(Channel.BUFFERED) - var playedCard1: Card? = null - var playedCard2: Card? = null + var playedCard1: Card? = null + var playedCard2: Card? = null - val player1Job = - launch { - channelCommand1.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player1))) - channelNotification1.receive().let { - assertIs(it).players shouldBeEqual setOf(player1) - } - channelNotification1.receive().let { - assertIs(it).player shouldBeEqual player2 - } - channelCommand1.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player1))) - channelNotification1.receive().let { - assertIs(it).player shouldBeEqual player2 - } - val player1Hand = - channelNotification1.receive().let { - assertIs(it).hand shouldHaveSize 7 - } - playedCard1 = player1Hand.first() - channelNotification1.receive().let { - assertIs(it).apply { - player shouldBeEqual player1 - } - } - channelCommand1.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player1, player1Hand.first()))) - - channelNotification1.receive().let { - assertIs(it).apply { - player shouldBeEqual player2 - } - } - - channelNotification1.receive().let { - assertIs(it).apply { - player shouldBeEqual player2 - card shouldBeEqual assertNotNull(playedCard2) - } - } - } - - val player2Job = - launch { - delay(100) - channelCommand2.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player2))) - channelNotification2.receive().let { - assertIs(it).players shouldBeEqual setOf(player1, player2) - } - channelNotification2.receive().let { - assertIs(it).player shouldBeEqual player1 - } - channelCommand2.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player2))) - val player2Hand = - channelNotification2.receive().let { - assertIs(it).hand shouldHaveSize 7 - } - channelNotification2.receive().let { - assertIs(it).apply { - player shouldBeEqual player1 - } - } - channelNotification2.receive().let { - assertIs(it).apply { - player shouldBeEqual player1 - card shouldBeEqual assertNotNull(playedCard1) - } - } - playedCard2 = player2Hand.first() - - channelNotification2.receive().let { - assertIs(it).apply { - player shouldBeEqual player2 - } - } - channelCommand2.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player2, player2Hand.first()))) - } - - koinApplication { modules(appKoinModule) }.koin.apply { - val commandHandler by inject() - val eventStore by inject() - val playerNotificationListener by inject() - ReactionEventListener(get(), get(), get()).init() - playerNotificationListener.startListening(channelNotification1, player1) - playerNotificationListener.startListening(channelNotification2, player2) - - GlobalScope.launch(Dispatchers.IO) { - commandHandler.handle(player1, channelCommand1, channelNotification1) - } - GlobalScope.launch(Dispatchers.IO) { - commandHandler.handle(player2, channelCommand2, channelNotification2) - } - - joinAll(player1Job, player2Job) - - val state = - ProjectionSnapshotRepositoryInMemory( - eventStore = eventStore, - initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) }, - applyToProjection = GameState::apply, - ).getLast(id) - - state.aggregateId shouldBeEqual id - assertTrue(state.isStarted) - state.players shouldBeEqual setOf(player1, player2) - state.readyPlayers shouldBeEqual setOf(player1, player2) - state.direction shouldBeEqual GameState.Direction.CLOCKWISE - assertNotNull(state.cardOnCurrentStack) shouldBeEqual GameState.LastCard(assertNotNull(playedCard2), player2) - } + val player1Job = + launch { + channelCommand1.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player1))) + channelNotification1.receive().let { + assertIs(it).players shouldBeEqual setOf(player1) } + channelNotification1.receive().let { + assertIs(it).player shouldBeEqual player2 + } + channelCommand1.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player1))) + channelNotification1.receive().let { + assertIs(it).player shouldBeEqual player2 + } + val player1Hand = + channelNotification1.receive().let { + assertIs(it).hand shouldHaveSize 7 + } + playedCard1 = player1Hand.first() + channelNotification1.receive().let { + assertIs(it).apply { + player shouldBeEqual player1 + } + } + channelCommand1.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player1, player1Hand.first()))) + + channelNotification1.receive().let { + assertIs(it).apply { + player shouldBeEqual player2 + } + } + + channelNotification1.receive().let { + assertIs(it).apply { + player shouldBeEqual player2 + card shouldBeEqual assertNotNull(playedCard2) + } + } + } + + val player2Job = + launch { + delay(100) + channelCommand2.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player2))) + channelNotification2.receive().let { + assertIs(it).players shouldBeEqual setOf(player1, player2) + } + channelNotification2.receive().let { + assertIs(it).player shouldBeEqual player1 + } + channelCommand2.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player2))) + val player2Hand = + channelNotification2.receive().let { + assertIs(it).hand shouldHaveSize 7 + } + channelNotification2.receive().let { + assertIs(it).apply { + player shouldBeEqual player1 + } + } + channelNotification2.receive().let { + assertIs(it).apply { + player shouldBeEqual player1 + card shouldBeEqual assertNotNull(playedCard1) + } + } + playedCard2 = player2Hand.first() + + channelNotification2.receive().let { + assertIs(it).apply { + player shouldBeEqual player2 + } + } + channelCommand2.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player2, player2Hand.first()))) + } + + koinApplication { modules(appKoinModule) }.koin.apply { + val commandHandler by inject() + val eventStore by inject() + val playerNotificationListener by inject() + ReactionEventListener(get(), get(), get()).init() + playerNotificationListener.startListening(channelNotification1, player1) + playerNotificationListener.startListening(channelNotification2, player2) + + GlobalScope.launch(Dispatchers.IO) { + commandHandler.handle(player1, channelCommand1, channelNotification1) + } + GlobalScope.launch(Dispatchers.IO) { + commandHandler.handle(player2, channelCommand2, channelNotification2) + } + + joinAll(player1Job, player2Job) + + val state = + ProjectionSnapshotRepositoryInMemory( + eventStore = eventStore, + initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) }, + applyToProjection = GameState::apply, + ).getLast(id) + + state.aggregateId shouldBeEqual id + assertTrue(state.isStarted) + state.players shouldBeEqual setOf(player1, player2) + state.readyPlayers shouldBeEqual setOf(player1, player2) + state.direction shouldBeEqual GameState.Direction.CLOCKWISE + assertNotNull(state.cardOnCurrentStack) shouldBeEqual GameState.LastCard(assertNotNull(playedCard2), player2) } - }) + } + } + }) diff --git a/src/test/kotlin/eventDemo/app/query/TestHttpClient.kt b/src/test/kotlin/eventDemo/app/query/TestHttpClient.kt index bea81bb..8dee0fd 100644 --- a/src/test/kotlin/eventDemo/app/query/TestHttpClient.kt +++ b/src/test/kotlin/eventDemo/app/query/TestHttpClient.kt @@ -6,10 +6,10 @@ import io.ktor.serialization.kotlinx.json.json import io.ktor.server.testing.ApplicationTestBuilder fun ApplicationTestBuilder.httpClient(): HttpClient = - createClient { - install(ContentNegotiation) { - json( - defaultJsonSerializer(), - ) - } + createClient { + install(ContentNegotiation) { + json( + defaultJsonSerializer(), + ) } + } diff --git a/src/test/kotlin/eventDemo/libs/FrameChannelConverterTest.kt b/src/test/kotlin/eventDemo/libs/FrameChannelConverterTest.kt index caa43a0..9082cbc 100644 --- a/src/test/kotlin/eventDemo/libs/FrameChannelConverterTest.kt +++ b/src/test/kotlin/eventDemo/libs/FrameChannelConverterTest.kt @@ -14,42 +14,42 @@ import kotlin.test.assertIs @Serializable data class CommandTest( - override val id: CommandId, + override val id: CommandId, ) : Command class FrameChannelConverterTest : - FunSpec({ + FunSpec({ - test("toObjectChannel") { - val uuid = "d737c631-76af-406e-bc29-f3e5b97226a5" - val id = CommandId(UUID.fromString(uuid)) - val jsonCommand = """{"id":"$uuid"}""" + test("toObjectChannel") { + val uuid = "d737c631-76af-406e-bc29-f3e5b97226a5" + val id = CommandId(UUID.fromString(uuid)) + val jsonCommand = """{"id":"$uuid"}""" - val channel = Channel() + val channel = Channel() - launch { - val commandChannel = toObjectChannel(channel) - commandChannel.receive().id shouldBeEqual id - channel.close() - } + launch { + val commandChannel = toObjectChannel(channel) + commandChannel.receive().id shouldBeEqual id + channel.close() + } - channel.send(Frame.Text(jsonCommand)) - } + channel.send(Frame.Text(jsonCommand)) + } - test("fromFrameChannel") { - val uuid = "d737c631-76af-406e-bc29-f3e5b97226a5" - val id = CommandId(UUID.fromString(uuid)) - val command = CommandTest(id) - val jsonCommand = """{"id":"$uuid"}""" + test("fromFrameChannel") { + val uuid = "d737c631-76af-406e-bc29-f3e5b97226a5" + val id = CommandId(UUID.fromString(uuid)) + val command = CommandTest(id) + val jsonCommand = """{"id":"$uuid"}""" - val channel = Channel() + val channel = Channel() - launch { - val commandChannel = fromFrameChannel(channel) - commandChannel.send(command) - commandChannel.close() - } + launch { + val commandChannel = fromFrameChannel(channel) + commandChannel.send(command) + commandChannel.close() + } - assertIs(channel.receive()).readText() shouldBeEqual jsonCommand - } - }) + assertIs(channel.receive()).readText() shouldBeEqual jsonCommand + } + }) diff --git a/src/test/kotlin/eventDemo/libs/command/CommandStreamChannelTest.kt b/src/test/kotlin/eventDemo/libs/command/CommandStreamChannelTest.kt index 7abc13b..5fae3cb 100644 --- a/src/test/kotlin/eventDemo/libs/command/CommandStreamChannelTest.kt +++ b/src/test/kotlin/eventDemo/libs/command/CommandStreamChannelTest.kt @@ -8,26 +8,26 @@ import kotlinx.serialization.Serializable @Serializable class CommandTest( - override val id: CommandId, + override val id: CommandId, ) : Command class CommandStreamChannelTest : - FunSpec({ + FunSpec({ - test("send and receive") { - val command = CommandTest(CommandId()) + test("send and receive") { + val command = CommandTest(CommandId()) - val channel = Channel() - val stream = - CommandStreamChannel(channel) + val channel = Channel() + val stream = + CommandStreamChannel(channel) - val spyCall: () -> Unit = mockk(relaxed = true) + val spyCall: () -> Unit = mockk(relaxed = true) - stream.blockAndProcess { - println("In action ${it.id}") - spyCall() - } - channel.send(command) - verify(exactly = 1) { spyCall() } - } - }) + stream.blockAndProcess { + println("In action ${it.id}") + spyCall() + } + channel.send(command) + verify(exactly = 1) { spyCall() } + } + }) diff --git a/src/test/kotlin/eventDemo/libs/event/EventBusInMemoryTest.kt b/src/test/kotlin/eventDemo/libs/event/EventBusInMemoryTest.kt index 6e4483f..ac92888 100644 --- a/src/test/kotlin/eventDemo/libs/event/EventBusInMemoryTest.kt +++ b/src/test/kotlin/eventDemo/libs/event/EventBusInMemoryTest.kt @@ -3,8 +3,8 @@ package eventDemo.libs.event import io.kotest.core.spec.style.FunSpec class EventBusInMemoryTest : - FunSpec({ + FunSpec({ - xtest("publish should call the subscribed functions") { } - xtest("publish should call the subscribed functions on the priority order") { } - }) + xtest("publish should call the subscribed functions") { } + xtest("publish should call the subscribed functions on the priority order") { } + }) diff --git a/src/test/kotlin/eventDemo/libs/event/EventStreamInMemoryTest.kt b/src/test/kotlin/eventDemo/libs/event/EventStreamInMemoryTest.kt index d85c282..f8133ff 100644 --- a/src/test/kotlin/eventDemo/libs/event/EventStreamInMemoryTest.kt +++ b/src/test/kotlin/eventDemo/libs/event/EventStreamInMemoryTest.kt @@ -3,14 +3,14 @@ package eventDemo.libs.event import io.kotest.core.spec.style.FunSpec class EventStreamInMemoryTest : - FunSpec({ + FunSpec({ - xtest("publish should be concurrently secure") { } + xtest("publish should be concurrently secure") { } - xtest("readLast should only return the event of aggregate") { } - xtest("readLast should return the last event of the aggregate") { } + xtest("readLast should only return the event of aggregate") { } + xtest("readLast should return the last event of the aggregate") { } - xtest("readLastOf should return the last event of the aggregate of the type") { } + xtest("readLastOf should return the last event of the aggregate of the type") { } - xtest("readAll should only return the event of aggregate") { } - }) + xtest("readAll should only return the event of aggregate") { } + }) diff --git a/src/test/kotlin/eventDemo/libs/event/VersionBuilderLocalTest.kt b/src/test/kotlin/eventDemo/libs/event/VersionBuilderLocalTest.kt index 2dab4b8..8636890 100644 --- a/src/test/kotlin/eventDemo/libs/event/VersionBuilderLocalTest.kt +++ b/src/test/kotlin/eventDemo/libs/event/VersionBuilderLocalTest.kt @@ -10,43 +10,43 @@ import java.util.UUID @JvmInline private value class IdTest( - override val id: UUID = UUID.randomUUID(), + override val id: UUID = UUID.randomUUID(), ) : AggregateId @OptIn(DelicateCoroutinesApi::class) class VersionBuilderLocalTest : - FunSpec({ + FunSpec({ - test("buildNextVersion") { - VersionBuilderLocal().run { - val id = IdTest() - buildNextVersion(id) shouldBeEqual 1 - buildNextVersion(id) shouldBeEqual 2 - buildNextVersion(IdTest()) shouldBeEqual 1 - buildNextVersion(id) shouldBeEqual 3 + test("buildNextVersion") { + VersionBuilderLocal().run { + val id = IdTest() + buildNextVersion(id) shouldBeEqual 1 + buildNextVersion(id) shouldBeEqual 2 + buildNextVersion(IdTest()) shouldBeEqual 1 + buildNextVersion(id) shouldBeEqual 3 + } + } + + test("buildNextVersion concurrently") { + val versionBuilder = VersionBuilderLocal() + val id = IdTest() + (1..20) + .map { + GlobalScope.launch { + (1..1000).map { + versionBuilder.buildNextVersion(id) } - } + } + }.joinAll() + versionBuilder.getLastVersion(id) shouldBeEqual 20 * 1000 + } - test("buildNextVersion concurrently") { - val versionBuilder = VersionBuilderLocal() - val id = IdTest() - (1..20) - .map { - GlobalScope.launch { - (1..1000).map { - versionBuilder.buildNextVersion(id) - } - } - }.joinAll() - versionBuilder.getLastVersion(id) shouldBeEqual 20 * 1000 - } - - test("getLastVersion") { - VersionBuilderLocal().run { - val id = IdTest() - getLastVersion(id) shouldBeEqual 0 - getLastVersion(id) shouldBeEqual 0 - getLastVersion(id) shouldBeEqual 0 - } - } - }) + test("getLastVersion") { + VersionBuilderLocal().run { + val id = IdTest() + getLastVersion(id) shouldBeEqual 0 + getLastVersion(id) shouldBeEqual 0 + getLastVersion(id) shouldBeEqual 0 + } + } + })