Compare commits
1
Commits
v0.2
...
3c85c344ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c85c344ce
|
+32
@@ -0,0 +1,32 @@
|
|||||||
|
<DataSourcesHistory>
|
||||||
|
<DataSourceFromHistory isRemovedFromProject="false">
|
||||||
|
<data-source source="LOCAL" name="event-demo@localhost" uuid="af2eabb1-64f7-49de-a94f-be1560baa96a">
|
||||||
|
<database-info product="PostgreSQL" version="18.4 (Debian 18.4-1.pgdg13+1)" jdbc-version="4.2" driver-name="PostgreSQL JDBC Driver" driver-version="42.7.3" dbms="POSTGRES" exact-version="18.4" exact-driver-version="42.7">
|
||||||
|
<identifier-quote-string>"</identifier-quote-string>
|
||||||
|
</database-info>
|
||||||
|
<case-sensitivity plain-identifiers="lower" quoted-identifiers="exact" />
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql://localhost:5432/event-demo</jdbc-url>
|
||||||
|
<secret-storage>master_key</secret-storage>
|
||||||
|
<user-name>event-demo</user-name>
|
||||||
|
<schema-mapping>
|
||||||
|
<introspection-scope>
|
||||||
|
<node negative="1">
|
||||||
|
<node kind="database" qname="@">
|
||||||
|
<node kind="schema" qname="@" />
|
||||||
|
</node>
|
||||||
|
<node kind="database" qname="event-demo">
|
||||||
|
<node kind="schema">
|
||||||
|
<name qname="auth" />
|
||||||
|
<name qname="game" />
|
||||||
|
</node>
|
||||||
|
</node>
|
||||||
|
</node>
|
||||||
|
</introspection-scope>
|
||||||
|
</schema-mapping>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</DataSourceFromHistory>
|
||||||
|
</DataSourcesHistory>
|
||||||
+21
-21
@@ -1,21 +1,19 @@
|
|||||||
@file:Suppress("PropertyName")
|
|
||||||
|
|
||||||
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
||||||
|
|
||||||
val ktor_version: String by project
|
val ktorVersion: Provider<String> = providers.gradleProperty("ktor_version")
|
||||||
val kotlin_version: String by project
|
val kotlinVersion: Provider<String> = providers.gradleProperty("kotlin_version")
|
||||||
val kotlin_serialization_version: String by project
|
val kotlinSerializationVersion: Provider<String> = providers.gradleProperty("kotlin_serialization_version")
|
||||||
val logback_version: String by project
|
val logbackVersion: Provider<String> = providers.gradleProperty("logback_version")
|
||||||
val koin_version: String by project
|
val koinVersion: Provider<String> = providers.gradleProperty("koin_version")
|
||||||
val kotlin_logging_version: String by project
|
val kotlinLoggingVersion: Provider<String> = providers.gradleProperty("kotlin_logging_version")
|
||||||
val kotest_version: String by project
|
val kotestVersion: Provider<String> = providers.gradleProperty("kotest_version")
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
application
|
application
|
||||||
kotlin("jvm") version "2.1.21"
|
kotlin("jvm") version "2.1.21"
|
||||||
id("io.ktor.plugin") version "3.5.1"
|
id("io.ktor.plugin") version "3.5.1"
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.10"
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.10"
|
||||||
id("org.jlleitschuh.gradle.ktlint") version "12.2.0"
|
id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
|
||||||
id("com.avast.gradle.docker-compose") version "0.17.12"
|
id("com.avast.gradle.docker-compose") version "0.17.12"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +27,7 @@ application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configure<KtlintExtension> {
|
configure<KtlintExtension> {
|
||||||
version.set("1.5.0")
|
version.set("1.8.0")
|
||||||
}
|
}
|
||||||
ktlint {
|
ktlint {
|
||||||
reporters {
|
reporters {
|
||||||
@@ -124,23 +122,25 @@ dependencies {
|
|||||||
implementation("io.ktor:ktor-server-data-conversion")
|
implementation("io.ktor:ktor-server-data-conversion")
|
||||||
implementation("io.ktor:ktor-client-content-negotiation")
|
implementation("io.ktor:ktor-client-content-negotiation")
|
||||||
implementation("io.ktor:ktor-client-auth")
|
implementation("io.ktor:ktor-client-auth")
|
||||||
implementation("ch.qos.logback:logback-classic:$logback_version")
|
implementation("ch.qos.logback:logback-classic:${logbackVersion.get()}")
|
||||||
implementation("io.insert-koin:koin-ktor:$koin_version")
|
implementation("io.insert-koin:koin-ktor:${koinVersion.get()}")
|
||||||
implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
|
implementation("io.insert-koin:koin-logger-slf4j:${koinVersion.get()}")
|
||||||
implementation("io.github.oshai:kotlin-logging-jvm:$kotlin_logging_version")
|
implementation("io.github.oshai:kotlin-logging-jvm:${kotlinLoggingVersion.get()}")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlin_serialization_version")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:${kotlinSerializationVersion.get()}")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
|
||||||
implementation("redis.clients:jedis:5.2.0")
|
implementation("redis.clients:jedis:5.2.0")
|
||||||
implementation("org.postgresql:postgresql:42.7.5")
|
implementation("org.postgresql:postgresql:42.7.13")
|
||||||
implementation("com.zaxxer:HikariCP:6.3.0")
|
implementation("com.zaxxer:HikariCP:6.3.0")
|
||||||
implementation("com.rabbitmq:amqp-client:5.25.0")
|
implementation("com.rabbitmq:amqp-client:5.25.0")
|
||||||
|
implementation("com.password4j:password4j:1.8.4")
|
||||||
|
|
||||||
// Force version of sub library (for security)
|
// Force version of sub library (for security)
|
||||||
implementation("commons-codec:commons-codec:1.13")
|
implementation("commons-codec:commons-codec:1.13")
|
||||||
|
|
||||||
testImplementation("io.kotest:kotest-extensions-koin:$kotest_version")
|
testImplementation("io.kotest:kotest-extensions-koin:${kotestVersion.get()}")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion.get()}")
|
||||||
testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version")
|
testImplementation("io.ktor:ktor-server-test-host-jvm:${ktorVersion.get()}")
|
||||||
testImplementation("io.kotest:kotest-runner-junit5:$kotest_version")
|
testImplementation("io.kotest:kotest-runner-junit5:${kotestVersion.get()}")
|
||||||
testImplementation("io.mockk:mockk:1.13.17")
|
testImplementation("io.mockk:mockk:1.13.17")
|
||||||
|
testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Exemple de structure
|
||||||
|
|
||||||
|
Les couches, du plus interne au plus externe
|
||||||
|
```
|
||||||
|
Domain (le cœur, ne dépend de RIEN d'externe)
|
||||||
|
↑
|
||||||
|
Application (orchestre le Domain, ne connaît pas l'infra concrète)
|
||||||
|
↑
|
||||||
|
Infrastructure (WebSocket, DB, event store — dépend de tout le reste)
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
└── contexts/
|
||||||
|
├── auth/
|
||||||
|
└── ...
|
||||||
|
└── game/
|
||||||
|
├── domain/ ← Le cœur métier, zéro dépendance externe
|
||||||
|
│ ├── game/
|
||||||
|
│ │ ├── Game.ts ← Aggregate Root
|
||||||
|
│ │ ├── Player.ts ← Entity interne
|
||||||
|
│ │ ├── Card.ts ← Entity
|
||||||
|
│ │ ├── Color.ts ← Value Object
|
||||||
|
│ │ ├── Deck.ts ← VO ou petite structure
|
||||||
|
│ │ └── errors/
|
||||||
|
│ │ ├── InvalidMoveError.ts
|
||||||
|
│ │ └── ColorChoiceRequiredError.ts
|
||||||
|
│ └── events/ ← Events de DOMAINE (internes)
|
||||||
|
│ ├── CardPlayed.ts
|
||||||
|
│ ├── CardDrawn.ts
|
||||||
|
│ ├── TurnPassed.ts
|
||||||
|
│ └── DomainEvent.ts ← interface/type de base
|
||||||
|
├── application/ ← Orchestration, cas d'usage
|
||||||
|
│ ├── commands/ ← Les Commandes (intentions)
|
||||||
|
│ │ ├── PlayCardCommand.ts
|
||||||
|
│ │ └── DrawCardCommand.ts
|
||||||
|
│ ├── handlers/ ← Un handler par commande
|
||||||
|
│ │ ├── PlayCardHandler.ts ← charge l'aggregate, appelle game.playCard(), save
|
||||||
|
│ │ └── DrawCardHandler.ts
|
||||||
|
│ ├── projections/ ← LA LOGIQUE de construction des projections
|
||||||
|
│ │ ├── GameSummaryProjector.kt ← écoute les events, met à jour la vue
|
||||||
|
│ │ └── PlayerStatsProjector.kt
|
||||||
|
│ └── ports/ ← INTERFACES seulement (le "hexagone")
|
||||||
|
│ ├── GameRepository.ts ← interface, pas d'implémentation
|
||||||
|
│ ├── EventPublisher.ts ← interface, pas d'implémentation
|
||||||
|
│ └── ProjectionStore.kt ← interface, où lire/écrire la projection
|
||||||
|
├── infrastructure/ ← Tout ce qui est technique/externe
|
||||||
|
│ ├── persistence/
|
||||||
|
│ │ ├── EventStoreGameRepository.ts ← implémente GameRepository
|
||||||
|
│ │ ├── EventStore.ts
|
||||||
|
│ │ ├── projections/
|
||||||
|
│ │ │ ├── GameSummaryProjectionStore.kt ← implémentation concrète (DB, table dédiée)
|
||||||
|
│ │ │ └── models/
|
||||||
|
│ │ │ └── GameSummaryView.kt ← structure de la vue elle-même
|
||||||
|
│ ├── websocket/
|
||||||
|
│ │ ├── WebSocketServer.ts
|
||||||
|
│ │ ├── connectionManager.ts ← Map<gameId, Map<playerId, WebSocket>>
|
||||||
|
│ │ └── commandRouter.ts ← reçoit le message brut, dispatch vers le bon handler
|
||||||
|
│ └── eventPublisher/
|
||||||
|
│ └── WebSocketEventPublisher.ts ← implémente EventPublisher, fait le broadcast
|
||||||
|
└── presentation/ ← Traduction vers/depuis le client (le fameux DTO layer)
|
||||||
|
├── clientEvents/
|
||||||
|
│ ├── ClientEvent.ts ← types des events envoyés au front
|
||||||
|
│ └── toClientEvent.ts ← fonction de traduction domain event → client event
|
||||||
|
└── clientCommands/
|
||||||
|
└── parseIncomingCommand.ts ← valide/parse le message brut du client → Command
|
||||||
|
```
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:3.3.4
|
image: traefik:3.7.9
|
||||||
command:
|
command:
|
||||||
- "--api.insecure=true"
|
- "--api.insecure=true"
|
||||||
- "--api.dashboard=true"
|
- "--api.dashboard=true"
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
create table event_stream (
|
create schema game;
|
||||||
|
create table game.game_event_stream (
|
||||||
id uuid not null primary key,
|
id uuid not null primary key,
|
||||||
aggregate_id uuid not null,
|
aggregate_id uuid not null,
|
||||||
version int not null,
|
version int not null,
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
create schema auth;
|
||||||
|
create table auth.user_event_stream (
|
||||||
|
id uuid not null primary key,
|
||||||
|
aggregate_id uuid not null,
|
||||||
|
version int not null,
|
||||||
|
data jsonb not null,
|
||||||
|
unique(aggregate_id, version)
|
||||||
|
);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
create table auth.user (
|
||||||
|
id uuid not null primary key,
|
||||||
|
username text not null,
|
||||||
|
unique(id),
|
||||||
|
unique(username)
|
||||||
|
);
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package eventDemo.adapter.infrastructure.event
|
|
||||||
|
|
||||||
import eventDemo.domain.entity.GameId
|
|
||||||
import eventDemo.domain.event.GameEventStore
|
|
||||||
import eventDemo.domain.event.event.GameEvent
|
|
||||||
import eventDemo.libs.event.EventStore
|
|
||||||
import eventDemo.libs.event.EventStoreInMemory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A stream to publish and read the played card event.
|
|
||||||
*/
|
|
||||||
class GameEventStoreInMemory :
|
|
||||||
GameEventStore,
|
|
||||||
EventStore<GameEvent, GameId> by EventStoreInMemory()
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package eventDemo.adapter.infrastructure.event
|
|
||||||
|
|
||||||
import eventDemo.domain.entity.GameId
|
|
||||||
import eventDemo.domain.event.GameEventStore
|
|
||||||
import eventDemo.domain.event.event.GameEvent
|
|
||||||
import eventDemo.libs.event.EventStore
|
|
||||||
import eventDemo.libs.event.EventStoreInPostgresql
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import javax.sql.DataSource
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A stream to publish and read the played card event.
|
|
||||||
*/
|
|
||||||
class GameEventStoreInPostgresql(
|
|
||||||
dataSource: DataSource,
|
|
||||||
) : GameEventStore,
|
|
||||||
EventStore<GameEvent, GameId> by EventStoreInPostgresql(
|
|
||||||
dataSource,
|
|
||||||
{ Json.encodeToString(it) },
|
|
||||||
{ Json.decodeFromString(it) },
|
|
||||||
)
|
|
||||||
-44
@@ -1,44 +0,0 @@
|
|||||||
package eventDemo.adapter.infrastructure.event.projection
|
|
||||||
|
|
||||||
import eventDemo.domain.entity.GameId
|
|
||||||
import eventDemo.domain.event.GameEventBus
|
|
||||||
import eventDemo.domain.event.projection.GameList
|
|
||||||
import eventDemo.domain.event.projection.GameListRepository
|
|
||||||
import eventDemo.domain.event.projection.GameProjectionBus
|
|
||||||
import eventDemo.domain.event.projection.GameState
|
|
||||||
import eventDemo.domain.event.projection.apply
|
|
||||||
import eventDemo.libs.event.projection.ProjectionRepositoryInMemory
|
|
||||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages [projections][GameList], their building and publication in the [bus][GameProjectionBus].
|
|
||||||
*/
|
|
||||||
class GameListRepositoryInMemory : GameListRepository {
|
|
||||||
private val projectionsRepository =
|
|
||||||
ProjectionRepositoryInMemory(
|
|
||||||
applyToProjection = GameList::apply,
|
|
||||||
initialStateBuilder = { aggregateId: GameId -> GameList(aggregateId) },
|
|
||||||
)
|
|
||||||
|
|
||||||
fun subscribeToBus(
|
|
||||||
projectionBus: GameProjectionBus,
|
|
||||||
eventBus: GameEventBus,
|
|
||||||
) {
|
|
||||||
// On new event was received, build projection and publish it to the projection bus
|
|
||||||
eventBus.subscribe { event ->
|
|
||||||
withLoggingContext("event" to event.toString()) {
|
|
||||||
projectionsRepository
|
|
||||||
.applyAndSave(event)
|
|
||||||
.also { projectionBus.publish(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last version of the [GameState] from the all eventStream.
|
|
||||||
*
|
|
||||||
* It fetches it from the local cache if possible, otherwise it builds it.
|
|
||||||
*/
|
|
||||||
override fun getList(): List<GameList> =
|
|
||||||
projectionsRepository.getList()
|
|
||||||
}
|
|
||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
package eventDemo.adapter.infrastructure.event.projection
|
|
||||||
|
|
||||||
import eventDemo.domain.entity.GameId
|
|
||||||
import eventDemo.domain.event.GameEventBus
|
|
||||||
import eventDemo.domain.event.projection.GameList
|
|
||||||
import eventDemo.domain.event.projection.GameListRepository
|
|
||||||
import eventDemo.domain.event.projection.GameProjectionBus
|
|
||||||
import eventDemo.domain.event.projection.GameState
|
|
||||||
import eventDemo.domain.event.projection.apply
|
|
||||||
import eventDemo.libs.event.projection.ProjectionRepositoryInRedis
|
|
||||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import redis.clients.jedis.UnifiedJedis
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages [projections][GameList], their building and publication in the [bus][GameProjectionBus].
|
|
||||||
*/
|
|
||||||
class GameListRepositoryInRedis(
|
|
||||||
jedis: UnifiedJedis,
|
|
||||||
) : GameListRepository {
|
|
||||||
private val projectionsRepository =
|
|
||||||
ProjectionRepositoryInRedis(
|
|
||||||
initialStateBuilder = { aggregateId: GameId -> GameList(aggregateId) },
|
|
||||||
projectionClass = GameList::class,
|
|
||||||
projectionToJson = { Json.encodeToString(GameList.serializer(), it) },
|
|
||||||
jsonToProjection = { Json.decodeFromString(GameList.serializer(), it) },
|
|
||||||
applyToProjection = GameList::apply,
|
|
||||||
jedis = jedis,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun subscribeToBus(
|
|
||||||
projectionBus: GameProjectionBus,
|
|
||||||
eventBus: GameEventBus,
|
|
||||||
) {
|
|
||||||
eventBus.subscribe { event ->
|
|
||||||
withLoggingContext("event" to event.toString()) {
|
|
||||||
projectionsRepository
|
|
||||||
.applyAndSave(event)
|
|
||||||
.also { projectionBus.publish(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last version of the [GameState] from the all eventStream.
|
|
||||||
*
|
|
||||||
* It fetches it from the local cache if possible, otherwise it builds it.
|
|
||||||
*/
|
|
||||||
override fun getList(): List<GameList> =
|
|
||||||
projectionsRepository.getList()
|
|
||||||
}
|
|
||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
package eventDemo.adapter.infrastructure.event.projection
|
|
||||||
|
|
||||||
import eventDemo.domain.entity.GameId
|
|
||||||
import eventDemo.domain.event.GameEventBus
|
|
||||||
import eventDemo.domain.event.projection.GameProjectionBus
|
|
||||||
import eventDemo.domain.event.projection.GameState
|
|
||||||
import eventDemo.domain.event.projection.GameStateRepository
|
|
||||||
import eventDemo.domain.event.projection.apply
|
|
||||||
import eventDemo.libs.event.projection.ProjectionRepositoryInMemory
|
|
||||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages [projections][GameState], their building and publication in the [bus][GameProjectionBus].
|
|
||||||
*/
|
|
||||||
class GameStateRepositoryInMemory : GameStateRepository {
|
|
||||||
private val projectionsRepository =
|
|
||||||
ProjectionRepositoryInMemory(
|
|
||||||
applyToProjection = GameState::apply,
|
|
||||||
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
|
|
||||||
)
|
|
||||||
|
|
||||||
fun subscribeToBus(
|
|
||||||
projectionBus: GameProjectionBus,
|
|
||||||
eventBus: GameEventBus,
|
|
||||||
) {
|
|
||||||
// On new event was received, build projection and publish it to the projection bus
|
|
||||||
eventBus.subscribe { event ->
|
|
||||||
withLoggingContext("event" to event.toString()) {
|
|
||||||
projectionsRepository
|
|
||||||
.applyAndSave(event)
|
|
||||||
.also { projectionBus.publish(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the [GameState].
|
|
||||||
*/
|
|
||||||
override fun get(gameId: GameId): GameState =
|
|
||||||
projectionsRepository.get(gameId)
|
|
||||||
}
|
|
||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
package eventDemo.adapter.infrastructure.event.projection
|
|
||||||
|
|
||||||
import eventDemo.domain.entity.GameId
|
|
||||||
import eventDemo.domain.event.GameEventBus
|
|
||||||
import eventDemo.domain.event.projection.GameProjectionBus
|
|
||||||
import eventDemo.domain.event.projection.GameState
|
|
||||||
import eventDemo.domain.event.projection.GameStateRepository
|
|
||||||
import eventDemo.domain.event.projection.apply
|
|
||||||
import eventDemo.libs.event.projection.ProjectionRepositoryInRedis
|
|
||||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import redis.clients.jedis.UnifiedJedis
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages [projections][GameState], their building and publication in the [bus][GameProjectionBus].
|
|
||||||
*/
|
|
||||||
class GameStateRepositoryInRedis(
|
|
||||||
jedis: UnifiedJedis,
|
|
||||||
) : GameStateRepository {
|
|
||||||
private val projectionsRepository =
|
|
||||||
ProjectionRepositoryInRedis(
|
|
||||||
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
|
|
||||||
projectionClass = GameState::class,
|
|
||||||
projectionToJson = { Json.encodeToString(GameState.serializer(), it) },
|
|
||||||
jsonToProjection = { Json.decodeFromString(GameState.serializer(), it) },
|
|
||||||
applyToProjection = GameState::apply,
|
|
||||||
jedis = jedis,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun subscribeToBus(
|
|
||||||
projectionBus: GameProjectionBus,
|
|
||||||
eventBus: GameEventBus,
|
|
||||||
) {
|
|
||||||
// On new event was received, build projection and publish it to the projection bus
|
|
||||||
eventBus.subscribe { event ->
|
|
||||||
withLoggingContext("event" to event.toString()) {
|
|
||||||
projectionsRepository
|
|
||||||
.applyAndSave(event)
|
|
||||||
.also { projectionBus.publish(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the [GameState].
|
|
||||||
*/
|
|
||||||
override fun get(gameId: GameId): GameState =
|
|
||||||
projectionsRepository.get(gameId)
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package eventDemo.adapter.presenter.query
|
|
||||||
|
|
||||||
import eventDemo.domain.command.GameCommandHandler
|
|
||||||
import eventDemo.domain.command.command.GameCommand
|
|
||||||
import eventDemo.domain.entity.GameId
|
|
||||||
import eventDemo.domain.event.projection.projectionListener.PlayerNotificationListener
|
|
||||||
import eventDemo.domain.notification.Notification
|
|
||||||
import eventDemo.libs.fromFrameChannel
|
|
||||||
import eventDemo.libs.toObjectChannel
|
|
||||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
|
||||||
import io.ktor.server.auth.authenticate
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.websocket.DefaultWebSocketServerSession
|
|
||||||
import io.ktor.server.websocket.webSocket
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
|
||||||
import kotlinx.coroutines.channels.SendChannel
|
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
fun Route.gameWebSocket(
|
|
||||||
playerNotificationListener: PlayerNotificationListener,
|
|
||||||
commandHandler: GameCommandHandler,
|
|
||||||
) {
|
|
||||||
authenticate {
|
|
||||||
webSocket("/games/new") {
|
|
||||||
runWebSocket(GameId(), commandHandler, playerNotificationListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
webSocket("/games/{id}") {
|
|
||||||
val gameId = GameId(UUID.fromString(call.parameters["id"]!!))
|
|
||||||
runWebSocket(gameId, commandHandler, playerNotificationListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
private fun DefaultWebSocketServerSession.runWebSocket(
|
|
||||||
gameId: GameId,
|
|
||||||
commandHandler: GameCommandHandler,
|
|
||||||
playerNotificationListener: PlayerNotificationListener,
|
|
||||||
) {
|
|
||||||
val currentPlayer = call.getPlayerCredentials()
|
|
||||||
val incomingFrameChannel: ReceiveChannel<GameCommand> = toObjectChannel(incoming)
|
|
||||||
val outgoingFrameChannel: SendChannel<Notification> = fromFrameChannel(outgoing)
|
|
||||||
withLoggingContext("currentPlayer" to currentPlayer.toString()) {
|
|
||||||
val notificationListener =
|
|
||||||
playerNotificationListener.startListening(
|
|
||||||
currentPlayer,
|
|
||||||
gameId,
|
|
||||||
) { outgoingFrameChannel.trySendBlocking(it) }
|
|
||||||
|
|
||||||
// TODO change GlobalScope
|
|
||||||
GlobalScope.launch {
|
|
||||||
commandHandler.handleIncomingPlayerCommands(
|
|
||||||
currentPlayer,
|
|
||||||
gameId,
|
|
||||||
incomingFrameChannel,
|
|
||||||
outgoingFrameChannel,
|
|
||||||
)
|
|
||||||
notificationListener.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package eventDemo.adapter.presenter.query
|
|
||||||
|
|
||||||
import eventDemo.domain.entity.Player
|
|
||||||
import io.ktor.server.application.ApplicationCall
|
|
||||||
import io.ktor.server.auth.jwt.JWTPrincipal
|
|
||||||
import io.ktor.server.auth.principal
|
|
||||||
|
|
||||||
internal fun ApplicationCall.getPlayerCredentials() =
|
|
||||||
principal<JWTPrincipal>()!!.run {
|
|
||||||
Player(
|
|
||||||
id = payload.getClaim("playerid").asString(),
|
|
||||||
name = payload.getClaim("username").asString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package eventDemo.adapter.presenter.query
|
|
||||||
|
|
||||||
import eventDemo.domain.entity.GameId
|
|
||||||
import eventDemo.domain.event.projection.GameStateRepository
|
|
||||||
import eventDemo.configuration.serializer.GameIdSerializer
|
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.resources.Resource
|
|
||||||
import io.ktor.server.auth.authenticate
|
|
||||||
import io.ktor.server.resources.get
|
|
||||||
import io.ktor.server.response.respond
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@Resource("/games/{id}")
|
|
||||||
class Game(
|
|
||||||
@Serializable(with = GameIdSerializer::class)
|
|
||||||
val id: GameId,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
@Resource("card/last")
|
|
||||||
class Card(
|
|
||||||
val game: Game,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@Resource("state")
|
|
||||||
class State(
|
|
||||||
val game: Game,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API routes to read the game state.
|
|
||||||
*/
|
|
||||||
fun Route.readTheGameState(gameStateRepository: GameStateRepository) {
|
|
||||||
authenticate {
|
|
||||||
// Read the last played card on the game.
|
|
||||||
get<Game.Card> { body ->
|
|
||||||
gameStateRepository
|
|
||||||
.get(body.game.id)
|
|
||||||
.cardOnCurrentStack
|
|
||||||
?.let { call.respond(it) }
|
|
||||||
?: call.response.status(HttpStatusCode.BadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the last played card on the game.
|
|
||||||
get<Game.State> { body ->
|
|
||||||
val state = gameStateRepository.get(body.game.id)
|
|
||||||
call.respond(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package eventDemo.configuration
|
||||||
|
|
||||||
|
import io.ktor.server.config.ApplicationConfig
|
||||||
|
|
||||||
|
data class Configuration(
|
||||||
|
val redisUrl: String,
|
||||||
|
val jwtSecret: String,
|
||||||
|
val postgresql: Postgresql,
|
||||||
|
val rabbitmq: RabbitMQ,
|
||||||
|
) {
|
||||||
|
data class Postgresql(
|
||||||
|
val url: String,
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RabbitMQ(
|
||||||
|
val url: String,
|
||||||
|
val port: Int,
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ApplicationConfig.configuration
|
||||||
|
get() =
|
||||||
|
Configuration(
|
||||||
|
redisUrl = getProperty("redis.url"),
|
||||||
|
jwtSecret = getProperty("jwt.secret"),
|
||||||
|
postgresql =
|
||||||
|
Configuration.Postgresql(
|
||||||
|
url = getProperty("postgresql.url"),
|
||||||
|
username = getProperty("postgresql.username"),
|
||||||
|
password = getProperty("postgresql.password"),
|
||||||
|
),
|
||||||
|
rabbitmq =
|
||||||
|
Configuration.RabbitMQ(
|
||||||
|
url = getProperty("rabbitmq.url"),
|
||||||
|
port = getProperty("rabbitmq.port").toInt(),
|
||||||
|
username = getProperty("rabbitmq.username"),
|
||||||
|
password = getProperty("rabbitmq.password"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun ApplicationConfig.getProperty(path: String): String =
|
||||||
|
propertyOrNull(path)?.getString() ?: error("You must set the $path")
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package eventDemo.configuration
|
|
||||||
|
|
||||||
import eventDemo.configuration.domain.configureGameListener
|
|
||||||
import eventDemo.configuration.ktor.configureHttpRouting
|
|
||||||
import eventDemo.configuration.ktor.configureKoin
|
|
||||||
import eventDemo.configuration.ktor.configureSecurity
|
|
||||||
import eventDemo.configuration.ktor.configureSerialization
|
|
||||||
import eventDemo.configuration.ktor.configureWebSockets
|
|
||||||
import eventDemo.configuration.route.declareHttpGameRoute
|
|
||||||
import eventDemo.configuration.route.declareWebSocketsGameRoute
|
|
||||||
import io.ktor.server.application.Application
|
|
||||||
import org.koin.ktor.ext.get
|
|
||||||
import org.koin.ktor.ext.getKoin
|
|
||||||
|
|
||||||
fun Application.configure() {
|
|
||||||
configureKoin()
|
|
||||||
|
|
||||||
configureSecurity()
|
|
||||||
|
|
||||||
configureSerialization()
|
|
||||||
|
|
||||||
configureWebSockets()
|
|
||||||
declareWebSocketsGameRoute(get(), get())
|
|
||||||
|
|
||||||
configureHttpRouting()
|
|
||||||
declareHttpGameRoute()
|
|
||||||
|
|
||||||
getKoin().configureGameListener()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package eventDemo.configuration
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.infrastructure.configure.configureAuthDi
|
||||||
|
import eventDemo.contexts.game.infrastructure.configuration.injections.application.configureGameDIApplication
|
||||||
|
import eventDemo.contexts.game.infrastructure.configuration.injections.infrastructure.configureGameDIInfrastructure
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
fun appKoinModule(config: Configuration) =
|
||||||
|
module {
|
||||||
|
configureDIDataSource(config)
|
||||||
|
configureAuthDi()
|
||||||
|
configureGameDIInfrastructure()
|
||||||
|
configureGameDIApplication()
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package eventDemo.configuration
|
||||||
|
|
||||||
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
|
import com.zaxxer.hikari.HikariConfig
|
||||||
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.scope.Scope
|
||||||
|
import org.koin.core.scope.ScopeCallback
|
||||||
|
import org.koin.dsl.bind
|
||||||
|
import redis.clients.jedis.JedisPooled
|
||||||
|
import redis.clients.jedis.UnifiedJedis
|
||||||
|
import javax.sql.DataSource
|
||||||
|
|
||||||
|
fun Module.configureDIDataSource(config: Configuration) {
|
||||||
|
// PostgreSQL (for EventStore)
|
||||||
|
single {
|
||||||
|
hikariDataSource(config)
|
||||||
|
.apply {
|
||||||
|
registerCallback(
|
||||||
|
object : ScopeCallback {
|
||||||
|
override fun onScopeClose(scope: Scope) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} bind DataSource::class
|
||||||
|
|
||||||
|
// Redis (for Projections)
|
||||||
|
single {
|
||||||
|
JedisPooled(config.redisUrl)
|
||||||
|
} bind UnifiedJedis::class
|
||||||
|
|
||||||
|
// RabbitMQ (for EventBus)
|
||||||
|
factory {
|
||||||
|
ConnectionFactory().apply {
|
||||||
|
host = config.rabbitmq.url
|
||||||
|
port = config.rabbitmq.port
|
||||||
|
username = config.rabbitmq.username
|
||||||
|
password = config.rabbitmq.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hikariDataSource(config: Configuration): HikariDataSource =
|
||||||
|
HikariConfig()
|
||||||
|
.apply {
|
||||||
|
jdbcUrl = config.postgresql.url
|
||||||
|
username = config.postgresql.username
|
||||||
|
password = config.postgresql.password
|
||||||
|
maximumPoolSize = 10
|
||||||
|
minimumIdle = 10
|
||||||
|
}.let {
|
||||||
|
HikariDataSource(it)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package eventDemo.configuration
|
||||||
|
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
import io.ktor.server.application.install
|
||||||
|
import org.koin.ktor.plugin.Koin
|
||||||
|
import org.koin.logger.slf4jLogger
|
||||||
|
|
||||||
|
fun Application.configureKoin() {
|
||||||
|
install(Koin) {
|
||||||
|
slf4jLogger()
|
||||||
|
|
||||||
|
modules(
|
||||||
|
appKoinModule(environment.config.configuration),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package eventDemo.configuration
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.infrastructure.configure.configureAuth
|
||||||
|
import eventDemo.contexts.game.infrastructure.configuration.ktor.configureUno
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
|
||||||
|
fun Application.configure() {
|
||||||
|
configureKoin()
|
||||||
|
configureAuth()
|
||||||
|
configureUno()
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package eventDemo.configuration.domain
|
|
||||||
|
|
||||||
import eventDemo.adapter.infrastructure.event.projection.GameListRepositoryInRedis
|
|
||||||
import eventDemo.adapter.infrastructure.event.projection.GameStateRepositoryInRedis
|
|
||||||
import eventDemo.domain.command.GameCommandHandler
|
|
||||||
import eventDemo.domain.event.projection.projectionListener.ReactionListener
|
|
||||||
import org.koin.core.Koin
|
|
||||||
|
|
||||||
fun Koin.configureGameListener() {
|
|
||||||
get<GameCommandHandler>()
|
|
||||||
.subscribeToBus(get())
|
|
||||||
|
|
||||||
get<GameStateRepositoryInRedis>()
|
|
||||||
.subscribeToBus(get(), get())
|
|
||||||
|
|
||||||
get<GameListRepositoryInRedis>()
|
|
||||||
.subscribeToBus(get(), get())
|
|
||||||
|
|
||||||
get<ReactionListener>()
|
|
||||||
.subscribeToBus(get())
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package eventDemo.configuration.injection
|
|
||||||
|
|
||||||
import org.koin.dsl.module
|
|
||||||
|
|
||||||
fun appKoinModule(config: Configuration) =
|
|
||||||
module {
|
|
||||||
configureDIBusiness()
|
|
||||||
configureDIInfrastructure(config)
|
|
||||||
configureDILibs()
|
|
||||||
configureDICommandActions()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Configuration(
|
|
||||||
val redisUrl: String,
|
|
||||||
val postgresql: Postgresql,
|
|
||||||
val rabbitmq: RabbitMQ,
|
|
||||||
) {
|
|
||||||
data class Postgresql(
|
|
||||||
val url: String,
|
|
||||||
val username: String,
|
|
||||||
val password: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class RabbitMQ(
|
|
||||||
val url: String,
|
|
||||||
val port: Int,
|
|
||||||
val username: String,
|
|
||||||
val password: String,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package eventDemo.configuration.injection
|
|
||||||
|
|
||||||
import eventDemo.domain.command.action.ICantPlay
|
|
||||||
import eventDemo.domain.command.action.IWantToJoinTheGame
|
|
||||||
import eventDemo.domain.command.action.IWantToPlayCard
|
|
||||||
import eventDemo.domain.command.action.IamReadyToPlay
|
|
||||||
import org.koin.core.module.Module
|
|
||||||
import org.koin.core.module.dsl.singleOf
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure all actions
|
|
||||||
*/
|
|
||||||
fun Module.configureDICommandActions() {
|
|
||||||
singleOf(::IWantToPlayCard)
|
|
||||||
singleOf(::IamReadyToPlay)
|
|
||||||
singleOf(::IWantToJoinTheGame)
|
|
||||||
singleOf(::ICantPlay)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package eventDemo.configuration.injection
|
|
||||||
|
|
||||||
import eventDemo.domain.command.GameCommandActionRunner
|
|
||||||
import eventDemo.domain.command.GameCommandHandler
|
|
||||||
import eventDemo.domain.event.GameEventHandler
|
|
||||||
import eventDemo.domain.event.projection.projectionListener.PlayerNotificationListener
|
|
||||||
import eventDemo.domain.event.projection.projectionListener.ReactionListener
|
|
||||||
import org.koin.core.module.Module
|
|
||||||
import org.koin.core.module.dsl.singleOf
|
|
||||||
|
|
||||||
fun Module.configureDIBusiness() {
|
|
||||||
single {
|
|
||||||
GameCommandHandler(get(), get(), get(), get())
|
|
||||||
}
|
|
||||||
singleOf(::GameEventHandler)
|
|
||||||
singleOf(::GameCommandActionRunner)
|
|
||||||
singleOf(::PlayerNotificationListener)
|
|
||||||
singleOf(::ReactionListener)
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package eventDemo.configuration.injection
|
|
||||||
|
|
||||||
import com.rabbitmq.client.ConnectionFactory
|
|
||||||
import com.zaxxer.hikari.HikariConfig
|
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
|
||||||
import eventDemo.adapter.infrastructure.event.GameEventBusInRabbinMQ
|
|
||||||
import eventDemo.adapter.infrastructure.event.GameEventStoreInPostgresql
|
|
||||||
import eventDemo.adapter.infrastructure.event.projection.GameListRepositoryInRedis
|
|
||||||
import eventDemo.adapter.infrastructure.event.projection.GameProjectionBusInRabbitMQ
|
|
||||||
import eventDemo.adapter.infrastructure.event.projection.GameStateRepositoryInRedis
|
|
||||||
import eventDemo.domain.event.GameEventBus
|
|
||||||
import eventDemo.domain.event.GameEventStore
|
|
||||||
import eventDemo.domain.event.projection.GameListRepository
|
|
||||||
import eventDemo.domain.event.projection.GameProjectionBus
|
|
||||||
import eventDemo.domain.event.projection.GameStateRepository
|
|
||||||
import org.koin.core.module.Module
|
|
||||||
import org.koin.core.module.dsl.singleOf
|
|
||||||
import org.koin.core.scope.Scope
|
|
||||||
import org.koin.core.scope.ScopeCallback
|
|
||||||
import org.koin.dsl.bind
|
|
||||||
import redis.clients.jedis.JedisPooled
|
|
||||||
import redis.clients.jedis.UnifiedJedis
|
|
||||||
import javax.sql.DataSource
|
|
||||||
|
|
||||||
fun Module.configureDIInfrastructure(config: Configuration) {
|
|
||||||
// Postgresql config
|
|
||||||
single {
|
|
||||||
JedisPooled(config.redisUrl)
|
|
||||||
} bind UnifiedJedis::class
|
|
||||||
|
|
||||||
single {
|
|
||||||
HikariConfig()
|
|
||||||
.apply {
|
|
||||||
jdbcUrl = config.postgresql.url
|
|
||||||
username = config.postgresql.username
|
|
||||||
password = config.postgresql.password
|
|
||||||
maximumPoolSize = 10
|
|
||||||
minimumIdle = 10
|
|
||||||
}.let {
|
|
||||||
HikariDataSource(it)
|
|
||||||
}.also { datasource ->
|
|
||||||
registerCallback(
|
|
||||||
object : ScopeCallback {
|
|
||||||
override fun onScopeClose(scope: Scope) {
|
|
||||||
datasource.close()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} bind DataSource::class
|
|
||||||
|
|
||||||
// RabbitMQ config
|
|
||||||
factory {
|
|
||||||
ConnectionFactory().apply {
|
|
||||||
host = config.rabbitmq.url
|
|
||||||
port = config.rabbitmq.port
|
|
||||||
username = config.rabbitmq.username
|
|
||||||
password = config.rabbitmq.password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
singleOf(::GameEventBusInRabbinMQ) bind GameEventBus::class
|
|
||||||
singleOf(::GameEventStoreInPostgresql) bind GameEventStore::class
|
|
||||||
singleOf(::GameProjectionBusInRabbitMQ) bind GameProjectionBus::class
|
|
||||||
|
|
||||||
single {
|
|
||||||
GameStateRepositoryInRedis(get())
|
|
||||||
} bind GameStateRepository::class
|
|
||||||
|
|
||||||
single {
|
|
||||||
GameListRepositoryInRedis(get())
|
|
||||||
} bind GameListRepository::class
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package eventDemo.configuration.injection
|
|
||||||
|
|
||||||
import eventDemo.libs.event.VersionBuilder
|
|
||||||
import eventDemo.libs.event.VersionBuilderLocal
|
|
||||||
import org.koin.core.module.Module
|
|
||||||
import org.koin.core.module.dsl.singleOf
|
|
||||||
import org.koin.dsl.bind
|
|
||||||
|
|
||||||
fun Module.configureDILibs() {
|
|
||||||
singleOf(::VersionBuilderLocal) bind VersionBuilder::class
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package eventDemo.configuration.ktor
|
|
||||||
|
|
||||||
import eventDemo.configuration.injection.Configuration
|
|
||||||
import eventDemo.configuration.injection.appKoinModule
|
|
||||||
import io.ktor.server.application.Application
|
|
||||||
import io.ktor.server.application.install
|
|
||||||
import io.ktor.server.config.ApplicationConfig
|
|
||||||
import org.koin.ktor.plugin.Koin
|
|
||||||
import org.koin.logger.slf4jLogger
|
|
||||||
|
|
||||||
fun Application.configureKoin() {
|
|
||||||
install(Koin) {
|
|
||||||
slf4jLogger()
|
|
||||||
|
|
||||||
modules(
|
|
||||||
appKoinModule(
|
|
||||||
environment.config.configuration(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ApplicationConfig.configuration() =
|
|
||||||
Configuration(
|
|
||||||
redisUrl = getProperty("redis.url"),
|
|
||||||
postgresql =
|
|
||||||
Configuration.Postgresql(
|
|
||||||
url = getProperty("postgresql.url"),
|
|
||||||
username = getProperty("postgresql.username"),
|
|
||||||
password = getProperty("postgresql.password"),
|
|
||||||
),
|
|
||||||
rabbitmq =
|
|
||||||
Configuration.RabbitMQ(
|
|
||||||
url = getProperty("rabbitmq.url"),
|
|
||||||
port = getProperty("rabbitmq.port").toInt(),
|
|
||||||
username = getProperty("rabbitmq.username"),
|
|
||||||
password = getProperty("rabbitmq.password"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun ApplicationConfig.getProperty(path: String): String =
|
|
||||||
propertyOrNull(path)?.getString() ?: error("You must set the $path")
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package eventDemo.configuration.route
|
|
||||||
|
|
||||||
import eventDemo.adapter.presenter.query.readGamesList
|
|
||||||
import eventDemo.adapter.presenter.query.readTheGameState
|
|
||||||
import io.ktor.server.application.Application
|
|
||||||
import io.ktor.server.routing.routing
|
|
||||||
import org.koin.ktor.ext.get
|
|
||||||
|
|
||||||
fun Application.declareHttpGameRoute() {
|
|
||||||
routing {
|
|
||||||
readTheGameState(this@declareHttpGameRoute.get())
|
|
||||||
readGamesList(this@declareHttpGameRoute.get())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package eventDemo.configuration.route
|
|
||||||
|
|
||||||
import eventDemo.adapter.presenter.query.gameWebSocket
|
|
||||||
import eventDemo.domain.command.GameCommandHandler
|
|
||||||
import eventDemo.domain.event.projection.projectionListener.PlayerNotificationListener
|
|
||||||
import io.ktor.server.application.Application
|
|
||||||
import io.ktor.server.routing.routing
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
fun Application.declareWebSocketsGameRoute(
|
|
||||||
playerNotificationListener: PlayerNotificationListener,
|
|
||||||
commandHandler: GameCommandHandler,
|
|
||||||
) {
|
|
||||||
routing {
|
|
||||||
gameWebSocket(playerNotificationListener, commandHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package eventDemo.contexts.auth.application.eventStores
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.application.ports.UserEventStore
|
||||||
|
import eventDemo.contexts.auth.domain.User
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
|
||||||
|
class UserEventStoreRepository(
|
||||||
|
val eventStore: UserEventStore,
|
||||||
|
) : UserRepository {
|
||||||
|
override fun get(id: UserId): User? {
|
||||||
|
val events =
|
||||||
|
eventStore
|
||||||
|
.getStream(id)
|
||||||
|
.readAll()
|
||||||
|
if (events.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return events.let { User.loadFromHistory(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun save(user: User) {
|
||||||
|
eventStore.append(user.recordedEvents)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package eventDemo.contexts.auth.application.eventStores
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.domain.User
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
|
||||||
|
interface UserRepository {
|
||||||
|
fun get(id: UserId): User?
|
||||||
|
|
||||||
|
fun save(user: User)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package eventDemo.contexts.auth.application.ports
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||||
|
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
|
||||||
|
interface UserEventStore : EventStore<UserEvent, UserId>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package eventDemo.contexts.auth.application.ports
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.infrastructure.persistence.projection.UserProjection
|
||||||
|
|
||||||
|
interface UserProjectionRepository {
|
||||||
|
fun getByUsername(username: String): UserProjection?
|
||||||
|
|
||||||
|
fun save(user: UserProjection)
|
||||||
|
|
||||||
|
fun getUserIfPasswordIsValid(
|
||||||
|
username: String,
|
||||||
|
rawPassword: String,
|
||||||
|
): UserProjection?
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package eventDemo.contexts.auth.domain
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.domain.events.NewUserCreatedEvent
|
||||||
|
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class User(
|
||||||
|
val id: UserId,
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
val version: Int,
|
||||||
|
val recordedEvents: Set<UserEvent>,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun createNewUser(
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
): User =
|
||||||
|
apply(NewUserCreatedEvent(username, password, version = 1))
|
||||||
|
|
||||||
|
fun apply(event: NewUserCreatedEvent): User =
|
||||||
|
User(
|
||||||
|
id = event.aggregateId,
|
||||||
|
username = event.username,
|
||||||
|
password = event.password,
|
||||||
|
version = event.version,
|
||||||
|
recordedEvents = setOf(event),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun loadFromHistory(events: Set<UserEvent>): User? =
|
||||||
|
events.fold(null as User?) { acc, event ->
|
||||||
|
when (event) {
|
||||||
|
is NewUserCreatedEvent -> apply(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package eventDemo.contexts.auth.domain.events
|
||||||
|
|
||||||
|
import eventDemo.libs.eventSource.EventId
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class NewUserCreatedEvent(
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
override val version: Int,
|
||||||
|
override val createdAt: Instant = Clock.System.now(),
|
||||||
|
override val aggregateId: UserId = UserId(),
|
||||||
|
) : UserEvent {
|
||||||
|
override val eventId: EventId = EventId()
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package eventDemo.contexts.auth.domain.events
|
||||||
|
|
||||||
|
import eventDemo.libs.eventSource.Event
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface UserEvent : Event<UserId>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package eventDemo.contexts.auth.infrastructure
|
||||||
|
|
||||||
|
import com.password4j.Hash
|
||||||
|
import com.password4j.Password
|
||||||
|
|
||||||
|
internal fun hashPassword(password: String): Hash =
|
||||||
|
Password.hash(password).addRandomSalt().withArgon2()
|
||||||
|
|
||||||
|
internal fun checkPassword(
|
||||||
|
password: String,
|
||||||
|
hash: Hash,
|
||||||
|
): Boolean =
|
||||||
|
Password.check(password, hash)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package eventDemo.contexts.auth.infrastructure.configure
|
||||||
|
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
|
||||||
|
fun Application.configureAuth() {
|
||||||
|
configureKtorAuth()
|
||||||
|
configureAuthRoutes()
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package eventDemo.contexts.auth.infrastructure.configure
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.application.eventStores.UserEventStoreRepository
|
||||||
|
import eventDemo.contexts.auth.application.eventStores.UserRepository
|
||||||
|
import eventDemo.contexts.auth.application.ports.UserEventStore
|
||||||
|
import eventDemo.contexts.auth.application.ports.UserProjectionRepository
|
||||||
|
import eventDemo.contexts.auth.infrastructure.persistence.eventStore.UserEventStoreInPostgresql
|
||||||
|
import eventDemo.contexts.auth.infrastructure.persistence.projection.UserProjectionRepositoryInPostgresql
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.bind
|
||||||
|
|
||||||
|
fun Module.configureAuthDi() {
|
||||||
|
singleOf(::UserEventStoreRepository) bind UserRepository::class
|
||||||
|
singleOf(::UserEventStoreInPostgresql) bind UserEventStore::class
|
||||||
|
singleOf(::UserProjectionRepositoryInPostgresql) bind UserProjectionRepository::class
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
package eventDemo.contexts.auth.infrastructure.configure
|
||||||
|
|
||||||
|
import eventDemo.configuration.configuration
|
||||||
|
import eventDemo.contexts.auth.application.eventStores.UserEventStoreRepository
|
||||||
|
import eventDemo.contexts.auth.application.ports.UserProjectionRepository
|
||||||
|
import eventDemo.contexts.auth.infrastructure.rest.createUserRoute
|
||||||
|
import eventDemo.contexts.auth.infrastructure.rest.loginRoute
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
import io.ktor.server.routing.routing
|
||||||
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
|
fun Application.configureAuthRoutes() {
|
||||||
|
val userRepository = get<UserEventStoreRepository>()
|
||||||
|
val userProjectionRepository = get<UserProjectionRepository>()
|
||||||
|
routing {
|
||||||
|
createUserRoute(userRepository)
|
||||||
|
loginRoute(environment.config.configuration.jwtSecret, userProjectionRepository)
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-23
@@ -1,24 +1,21 @@
|
|||||||
package eventDemo.configuration.ktor
|
package eventDemo.contexts.auth.infrastructure.configure
|
||||||
|
|
||||||
import com.auth0.jwt.JWT
|
import com.auth0.jwt.JWT
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
import eventDemo.domain.entity.Player
|
import eventDemo.configuration.configuration
|
||||||
|
import eventDemo.contexts.auth.domain.User
|
||||||
|
import eventDemo.contexts.auth.infrastructure.persistence.projection.UserProjection
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.auth.authentication
|
import io.ktor.server.auth.authentication
|
||||||
import io.ktor.server.auth.jwt.JWTPrincipal
|
import io.ktor.server.auth.jwt.JWTPrincipal
|
||||||
import io.ktor.server.auth.jwt.jwt
|
import io.ktor.server.auth.jwt.jwt
|
||||||
import io.ktor.server.response.respond
|
import io.ktor.server.response.respond
|
||||||
import io.ktor.server.routing.post
|
|
||||||
import io.ktor.server.routing.routing
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
private const val JWT_ISSUER = "PlayCardGame"
|
fun Application.configureKtorAuth() {
|
||||||
|
val jwtSecret = environment.config.configuration.jwtSecret
|
||||||
fun Application.configureSecurity() {
|
|
||||||
val jwtSecret = environment.config.propertyOrNull("jwt.secret")?.getString() ?: error("You must set a jwt secret")
|
|
||||||
|
|
||||||
authentication {
|
authentication {
|
||||||
jwt {
|
jwt {
|
||||||
realm = "Play card game"
|
realm = "Play card game"
|
||||||
@@ -29,7 +26,11 @@ fun Application.configureSecurity() {
|
|||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
validate { credential ->
|
validate { credential ->
|
||||||
if (credential.payload.getClaim("username").asString() != "") {
|
if (credential.payload
|
||||||
|
.getClaim("username")
|
||||||
|
.asString()
|
||||||
|
.isNotEmpty()
|
||||||
|
) {
|
||||||
JWTPrincipal(credential.payload)
|
JWTPrincipal(credential.payload)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -40,22 +41,25 @@ fun Application.configureSecurity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
routing {
|
|
||||||
post("login/{username}") {
|
|
||||||
val username = call.parameters["username"]!!
|
|
||||||
val player = Player(name = username)
|
|
||||||
|
|
||||||
call.respond(hashMapOf("token" to player.makeJwt(jwtSecret)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Player.makeJwt(jwtSecret: String): String =
|
private const val JWT_ISSUER = "PlayCardGame"
|
||||||
|
|
||||||
|
fun UserProjection.makeJwt(jwtSecret: String): String =
|
||||||
|
makeJwt(jwtSecret, id, username)
|
||||||
|
|
||||||
|
fun User.makeJwt(jwtSecret: String): String =
|
||||||
|
makeJwt(jwtSecret, id, username)
|
||||||
|
|
||||||
|
fun makeJwt(
|
||||||
|
jwtSecret: String,
|
||||||
|
id: UserId,
|
||||||
|
username: String,
|
||||||
|
): String =
|
||||||
JWT
|
JWT
|
||||||
.create()
|
.create()
|
||||||
.withIssuer(JWT_ISSUER)
|
.withIssuer(JWT_ISSUER)
|
||||||
.withClaim("username", name)
|
.withClaim("username", username)
|
||||||
.withPayload(Json.encodeToString(this))
|
.withClaim("userid", id.toString())
|
||||||
.withExpiresAt(Date(System.currentTimeMillis() + 60000))
|
.withExpiresAt(Date(System.currentTimeMillis() + 60000))
|
||||||
.sign(Algorithm.HMAC256(jwtSecret))
|
.sign(Algorithm.HMAC256(jwtSecret))
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package eventDemo.contexts.auth.infrastructure.persistence.eventStore
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.application.ports.UserEventStore
|
||||||
|
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||||
|
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||||
|
import eventDemo.libs.eventSource.eventStore.EventStoreInMemory
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stream to publish and read the user events.
|
||||||
|
*/
|
||||||
|
class UserEventStoreInMemory :
|
||||||
|
UserEventStore,
|
||||||
|
EventStore<UserEvent, UserId> by EventStoreInMemory()
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package eventDemo.contexts.auth.infrastructure.persistence.eventStore
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.application.ports.UserEventStore
|
||||||
|
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||||
|
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||||
|
import eventDemo.libs.eventSource.eventStore.EventStoreInPostgresql
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import javax.sql.DataSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stream to publish and read the user events.
|
||||||
|
*/
|
||||||
|
class UserEventStoreInPostgresql(
|
||||||
|
dataSource: DataSource,
|
||||||
|
) : UserEventStore,
|
||||||
|
EventStore<UserEvent, UserId> by EventStoreInPostgresql(
|
||||||
|
dataSource,
|
||||||
|
{ Json.encodeToString(it) },
|
||||||
|
{ Json.decodeFromString(it) },
|
||||||
|
"auth.user_event_stream",
|
||||||
|
)
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package eventDemo.contexts.auth.infrastructure.persistence.projection
|
||||||
|
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
|
||||||
|
data class UserProjection(
|
||||||
|
val id: UserId,
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
)
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package eventDemo.contexts.auth.infrastructure.persistence.projection
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.application.ports.UserProjectionRepository
|
||||||
|
import eventDemo.contexts.auth.infrastructure.checkPassword
|
||||||
|
import eventDemo.contexts.auth.infrastructure.hashPassword
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.sql.DataSource
|
||||||
|
|
||||||
|
class UserProjectionRepositoryInPostgresql(
|
||||||
|
val dataSource: DataSource,
|
||||||
|
) : UserProjectionRepository {
|
||||||
|
override fun getByUsername(username: String): UserProjection? =
|
||||||
|
dataSource.connection
|
||||||
|
.prepareStatement(
|
||||||
|
"""
|
||||||
|
select id, username
|
||||||
|
from auth."user"
|
||||||
|
where id = ?;
|
||||||
|
""".trimIndent(),
|
||||||
|
).use {
|
||||||
|
it.setObject(1, username)
|
||||||
|
it.executeQuery()
|
||||||
|
}.use { resultSet ->
|
||||||
|
if (resultSet.next()) {
|
||||||
|
UserProjection(
|
||||||
|
id = UserId(UUID.fromString(resultSet.getString("id"))),
|
||||||
|
username = resultSet.getString("username"),
|
||||||
|
password = resultSet.getString("password"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun save(user: UserProjection) {
|
||||||
|
dataSource.connection.use { connection ->
|
||||||
|
connection
|
||||||
|
.prepareStatement(
|
||||||
|
"""
|
||||||
|
insert into auth.user (id, username)
|
||||||
|
values (?, ?)
|
||||||
|
""".trimIndent(),
|
||||||
|
).use {
|
||||||
|
it.setObject(1, user.id)
|
||||||
|
it.setString(2, user.username)
|
||||||
|
it.executeUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserIfPasswordIsValid(
|
||||||
|
username: String,
|
||||||
|
rawPassword: String,
|
||||||
|
): UserProjection? {
|
||||||
|
val user = getByUsername(username) ?: return null
|
||||||
|
val isValid = checkPassword(rawPassword, hashPassword(user.password))
|
||||||
|
if (!isValid) return null
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package eventDemo.contexts.auth.infrastructure.rest
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.application.ports.UserProjectionRepository
|
||||||
|
import eventDemo.contexts.auth.infrastructure.configure.makeJwt
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.post
|
||||||
|
|
||||||
|
fun Route.loginRoute(
|
||||||
|
jwtSecret: String,
|
||||||
|
userProjectionRepository: UserProjectionRepository,
|
||||||
|
) {
|
||||||
|
post("login/{username}") {
|
||||||
|
val username = call.parameters["username"]!!
|
||||||
|
val rawPassword = call.parameters["password"]!!
|
||||||
|
|
||||||
|
val userProjection =
|
||||||
|
userProjectionRepository.getUserIfPasswordIsValid(username, rawPassword)
|
||||||
|
?: return@post call.respond(HttpStatusCode.BadRequest)
|
||||||
|
|
||||||
|
call.respond(hashMapOf("token" to userProjection.makeJwt(jwtSecret)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package eventDemo.contexts.auth.infrastructure.rest
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.application.eventStores.UserRepository
|
||||||
|
import eventDemo.contexts.auth.domain.User
|
||||||
|
import eventDemo.contexts.auth.infrastructure.hashPassword
|
||||||
|
import io.ktor.resources.Resource
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.post
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Resource("/users")
|
||||||
|
class Users {
|
||||||
|
@Serializable
|
||||||
|
@Resource("/create")
|
||||||
|
class Create(
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API routes to show all games.
|
||||||
|
*/
|
||||||
|
fun Route.createUserRoute(userRepository: UserRepository) {
|
||||||
|
authenticate {
|
||||||
|
// Create a new User, and return there ID
|
||||||
|
post<Users.Create> {
|
||||||
|
val passwordHash = hashPassword(it.password)
|
||||||
|
val user = User.createNewUser(it.username, passwordHash.result)
|
||||||
|
userRepository.save(user)
|
||||||
|
call.respond(
|
||||||
|
object {
|
||||||
|
val id = user.id.toString()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package eventDemo.contexts.game.application.channels
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.application.command.models.GameCommand
|
||||||
|
import eventDemo.contexts.game.application.notification.CommandSubscriber
|
||||||
|
import eventDemo.contexts.game.application.notification.EventToNotificationSubscriber
|
||||||
|
import eventDemo.contexts.game.application.notification.models.Notification
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
|
|
||||||
|
class GameChannelsSubscriber(
|
||||||
|
private val eventToNotificationSubscriber: EventToNotificationSubscriber,
|
||||||
|
private val commandSubscriber: CommandSubscriber,
|
||||||
|
) {
|
||||||
|
@DelicateCoroutinesApi
|
||||||
|
fun subscribePlayerToGameChannels(
|
||||||
|
gameId: GameId,
|
||||||
|
userId: UserId,
|
||||||
|
incomingCommandChannel: ReceiveChannel<GameCommand>,
|
||||||
|
sendNotificationChannel: SendChannel<Notification>,
|
||||||
|
) {
|
||||||
|
val sub =
|
||||||
|
eventToNotificationSubscriber.subscribeToEventsAndSendNotification(
|
||||||
|
gameId = gameId,
|
||||||
|
currentUserId = userId,
|
||||||
|
outgoingFrameChannel = sendNotificationChannel,
|
||||||
|
)
|
||||||
|
|
||||||
|
commandSubscriber
|
||||||
|
.subscribe(
|
||||||
|
currentUserId = userId,
|
||||||
|
incomingFrameChannel = incomingCommandChannel,
|
||||||
|
).invokeOnCompletion { sub.close() }
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package eventDemo.domain.command
|
package eventDemo.contexts.game.application.command.handlers
|
||||||
|
|
||||||
class CommandException(
|
class CommandException(
|
||||||
override val message: String,
|
override val message: String,
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package eventDemo.contexts.game.application.command.handlers
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.application.command.models.GameCommand
|
||||||
|
import eventDemo.contexts.game.application.command.models.JoinTheGameCommand
|
||||||
|
import eventDemo.contexts.game.application.command.models.PlayCardCommand
|
||||||
|
import eventDemo.contexts.game.application.command.models.ReadyToPlayCommand
|
||||||
|
import eventDemo.contexts.game.application.command.models.TakeCartFromDrawPileCommand
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
|
class GameCommandHandlerDispatcher(
|
||||||
|
private val playCardHandler: PlayCardHandler,
|
||||||
|
private val readyToPlayHandler: ReadyToPlayHandler,
|
||||||
|
private val joinTheGameHandler: JoinTheGameHandler,
|
||||||
|
private val takeCartFromDrawPileHandler: TakeCartFromDrawPileHandler,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val lock: MutableMap<GameId, String> = Collections.synchronizedMap(mutableMapOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispatch(command: GameCommand) {
|
||||||
|
synchronized(lock.getOrPut(command.payload.aggregateId) { command.payload.aggregateId.toString() }) {
|
||||||
|
when (command) {
|
||||||
|
is JoinTheGameCommand -> joinTheGameHandler.handle(command)
|
||||||
|
is ReadyToPlayCommand -> readyToPlayHandler.handle(command)
|
||||||
|
is PlayCardCommand -> playCardHandler.handle(command)
|
||||||
|
is TakeCartFromDrawPileCommand -> takeCartFromDrawPileHandler.handle(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
package eventDemo.contexts.game.application.command.handlers
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.application.command.models.GameCommand
|
||||||
|
import eventDemo.contexts.game.application.eventStores.GameRepository
|
||||||
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||||
|
import eventDemo.libs.command.Command
|
||||||
|
import eventDemo.libs.eventSource.eventStore.VersionConflictException
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
sealed interface CommandHandler<C : Command> {
|
||||||
|
fun handle(command: C)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class GameEventManager(
|
||||||
|
private val gameRepository: GameRepository,
|
||||||
|
private val gameEventBus: GameEventBus,
|
||||||
|
) {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
fun GameCommand.getGame(): Game =
|
||||||
|
gameRepository.get(payload.aggregateId) ?: error("Game not found")
|
||||||
|
|
||||||
|
fun GameEvent.getGame(): Game =
|
||||||
|
gameRepository.get(aggregateId) ?: error("Game not found")
|
||||||
|
|
||||||
|
@Throws(VersionConflictException::class)
|
||||||
|
protected fun Game.saveEvents(): Game {
|
||||||
|
gameRepository.save(this)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun Game.publishEvents(): Game {
|
||||||
|
gameEventBus.publish(recordedEvents)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun <G : Game> Game.isStatusOrFail(
|
||||||
|
kClass: KClass<G>,
|
||||||
|
message: String,
|
||||||
|
): G {
|
||||||
|
if (kClass.isInstance(this)) {
|
||||||
|
return this as G
|
||||||
|
} else {
|
||||||
|
throw CommandException(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun <T> retry(
|
||||||
|
mapAttempts: Int = 5,
|
||||||
|
block: () -> T,
|
||||||
|
): T =
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} catch (e: VersionConflictException) {
|
||||||
|
if (mapAttempts > 0) {
|
||||||
|
logger.warn { "retry after version conflict (attempts left: $mapAttempts)" }
|
||||||
|
retry(mapAttempts - 1, block)
|
||||||
|
} else {
|
||||||
|
logger.error { "Version conflict retry failed" }
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package eventDemo.contexts.game.application.command.handlers
|
||||||
|
|
||||||
|
import eventDemo.contexts.auth.application.eventStores.UserRepository
|
||||||
|
import eventDemo.contexts.game.application.command.models.JoinTheGameCommand
|
||||||
|
import eventDemo.contexts.game.application.eventStores.GameRepository
|
||||||
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command to perform an action to play a new card
|
||||||
|
*/
|
||||||
|
class JoinTheGameHandler(
|
||||||
|
gameRepository: GameRepository,
|
||||||
|
gameEventBus: GameEventBus,
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
) : GameEventManager(gameRepository, gameEventBus),
|
||||||
|
CommandHandler<JoinTheGameCommand> {
|
||||||
|
override fun handle(command: JoinTheGameCommand) {
|
||||||
|
val user = userRepository.get(command.userId) ?: error("User with id ${command.userId} doesn't exist")
|
||||||
|
retry {
|
||||||
|
command
|
||||||
|
.getGame()
|
||||||
|
.isStatusOrFail(GameCreated::class, "The game is started")
|
||||||
|
.userJoinTheGame(
|
||||||
|
userId = command.userId,
|
||||||
|
name = user.username,
|
||||||
|
).saveEvents()
|
||||||
|
.publishEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package eventDemo.contexts.game.application.command.handlers
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.application.command.models.PlayCardCommand
|
||||||
|
import eventDemo.contexts.game.application.eventStores.GameRepository
|
||||||
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command to perform an action to play a new card
|
||||||
|
*/
|
||||||
|
class PlayCardHandler(
|
||||||
|
gameRepository: GameRepository,
|
||||||
|
gameEventBus: GameEventBus,
|
||||||
|
) : GameEventManager(gameRepository, gameEventBus),
|
||||||
|
CommandHandler<PlayCardCommand> {
|
||||||
|
override fun handle(command: PlayCardCommand) {
|
||||||
|
command
|
||||||
|
.getGame()
|
||||||
|
.isStatusOrFail(GameStarted::class, "The game is not started")
|
||||||
|
.playTheCard(
|
||||||
|
card = command.payload.card,
|
||||||
|
playerId = command.payload.playerId,
|
||||||
|
chosenColor = command.payload.chosenColor,
|
||||||
|
).saveEvents()
|
||||||
|
.publishEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package eventDemo.contexts.game.application.command.handlers
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.application.command.models.ReadyToPlayCommand
|
||||||
|
import eventDemo.contexts.game.application.eventStores.GameRepository
|
||||||
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command to set as ready to play
|
||||||
|
*/
|
||||||
|
class ReadyToPlayHandler(
|
||||||
|
gameRepository: GameRepository,
|
||||||
|
gameEventBus: GameEventBus,
|
||||||
|
) : GameEventManager(gameRepository, gameEventBus),
|
||||||
|
CommandHandler<ReadyToPlayCommand> {
|
||||||
|
override fun handle(command: ReadyToPlayCommand) {
|
||||||
|
command
|
||||||
|
.getGame()
|
||||||
|
.isStatusOrFail(GameCreated::class, "The game is started")
|
||||||
|
.setReadyPlayer(command.payload.playerId)
|
||||||
|
.saveEvents()
|
||||||
|
.publishEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package eventDemo.contexts.game.application.command.handlers
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.application.command.models.TakeCartFromDrawPileCommand
|
||||||
|
import eventDemo.contexts.game.application.eventStores.GameRepository
|
||||||
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command to draw card on draw pile.
|
||||||
|
*
|
||||||
|
* Is can be triggered when you cannot play any card in your hand.
|
||||||
|
*/
|
||||||
|
class TakeCartFromDrawPileHandler(
|
||||||
|
gameRepository: GameRepository,
|
||||||
|
gameEventBus: GameEventBus,
|
||||||
|
) : GameEventManager(gameRepository, gameEventBus),
|
||||||
|
CommandHandler<TakeCartFromDrawPileCommand> {
|
||||||
|
override fun handle(command: TakeCartFromDrawPileCommand) {
|
||||||
|
command
|
||||||
|
.getGame()
|
||||||
|
.isStatusOrFail(GameStarted::class, "The game is not started")
|
||||||
|
.playerTakeCartFromDrawPile(command.payload.playerId, 1)
|
||||||
|
.saveEvents()
|
||||||
|
.publishEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package eventDemo.contexts.game.application.command.models
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
|
||||||
|
import eventDemo.libs.command.Command
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface GameCommand : Command {
|
||||||
|
val userId: UserId
|
||||||
|
val payload: Payload
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface Payload {
|
||||||
|
@Serializable(with = GameIdSerializer::class)
|
||||||
|
val aggregateId: GameId
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-5
@@ -1,22 +1,24 @@
|
|||||||
package eventDemo.domain.command.command
|
package eventDemo.contexts.game.application.command.models
|
||||||
|
|
||||||
import eventDemo.domain.entity.GameId
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
import eventDemo.domain.entity.Player
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
|
||||||
import eventDemo.libs.command.CommandId
|
import eventDemo.libs.command.CommandId
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command to perform an action to play a new card
|
* A command to perform an action to play a new card
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ICantPlayCommand(
|
data class JoinTheGameCommand(
|
||||||
|
override val userId: UserId,
|
||||||
override val payload: Payload,
|
override val payload: Payload,
|
||||||
) : GameCommand {
|
) : GameCommand {
|
||||||
override val id: CommandId = CommandId()
|
override val id: CommandId = CommandId()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Payload(
|
data class Payload(
|
||||||
|
@Serializable(with = GameIdSerializer::class)
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
override val player: Player,
|
|
||||||
) : GameCommand.Payload
|
) : GameCommand.Payload
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package eventDemo.contexts.game.application.command.models
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.Card
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
|
||||||
|
import eventDemo.libs.command.CommandId
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command to perform an action to play a new card
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class PlayCardCommand(
|
||||||
|
override val userId: UserId,
|
||||||
|
override val payload: Payload,
|
||||||
|
) : GameCommand {
|
||||||
|
override val id: CommandId = CommandId()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Payload(
|
||||||
|
@Serializable(with = GameIdSerializer::class)
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
@Serializable(with = PlayerIdSerializer::class)
|
||||||
|
val playerId: Player.PlayerId,
|
||||||
|
val card: Card,
|
||||||
|
val chosenColor: Card.Color?,
|
||||||
|
) : GameCommand.Payload
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package eventDemo.contexts.game.application.command.models
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
|
||||||
|
import eventDemo.libs.command.CommandId
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command to set as ready to play
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class ReadyToPlayCommand(
|
||||||
|
override val userId: UserId,
|
||||||
|
override val payload: Payload,
|
||||||
|
) : GameCommand {
|
||||||
|
override val id: CommandId = CommandId()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Payload(
|
||||||
|
@Serializable(with = GameIdSerializer::class)
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
@Serializable(with = PlayerIdSerializer::class)
|
||||||
|
val playerId: Player.PlayerId,
|
||||||
|
) : GameCommand.Payload
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package eventDemo.contexts.game.application.command.models
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
|
||||||
|
import eventDemo.libs.command.CommandId
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command to perform an action to play a new card
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class TakeCartFromDrawPileCommand(
|
||||||
|
override val userId: UserId,
|
||||||
|
override val payload: Payload,
|
||||||
|
) : GameCommand {
|
||||||
|
override val id: CommandId = CommandId()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Payload(
|
||||||
|
@Serializable(with = GameIdSerializer::class)
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
@Serializable(with = PlayerIdSerializer::class)
|
||||||
|
val playerId: Player.PlayerId,
|
||||||
|
) : GameCommand.Payload
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package eventDemo.contexts.game.application.eventStores
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.application.ports.GameEventStore
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||||
|
import eventDemo.libs.eventSource.eventStore.VersionConflictException
|
||||||
|
|
||||||
|
class GameEventStoreRepository(
|
||||||
|
val eventStore: GameEventStore,
|
||||||
|
) : GameRepository {
|
||||||
|
override fun get(id: GameId): Game? {
|
||||||
|
val events =
|
||||||
|
eventStore
|
||||||
|
.getStream(id)
|
||||||
|
.readAll()
|
||||||
|
if (events.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return events.let { Game.loadFromHistory(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(VersionConflictException::class)
|
||||||
|
override fun save(game: Game) {
|
||||||
|
eventStore.append(game.recordedEvents)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package eventDemo.contexts.game.application.eventStores
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.GameInit
|
||||||
|
import eventDemo.libs.eventSource.eventStore.VersionConflictException
|
||||||
|
|
||||||
|
interface GameRepository {
|
||||||
|
fun get(id: GameId): Game?
|
||||||
|
|
||||||
|
@Throws(VersionConflictException::class)
|
||||||
|
fun save(game: Game)
|
||||||
|
|
||||||
|
fun getOrCreate(gameId: GameId): Game =
|
||||||
|
get(gameId) ?: create(gameId)
|
||||||
|
|
||||||
|
fun create(gameId: GameId = GameId()): GameCreated =
|
||||||
|
GameInit.createNewGame(gameId).also { save(it) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package eventDemo.contexts.game.application.logging
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.withLoggingContext
|
||||||
|
|
||||||
|
inline fun <T> withLoggingContext(
|
||||||
|
vararg pair: Pair<LoggingContextKeys, *>,
|
||||||
|
body: () -> T,
|
||||||
|
): T =
|
||||||
|
withLoggingContext(
|
||||||
|
*pair
|
||||||
|
.map {
|
||||||
|
it.first.name to it.second.toString()
|
||||||
|
}.toTypedArray(),
|
||||||
|
restorePrevious = true,
|
||||||
|
body = body,
|
||||||
|
)
|
||||||
|
|
||||||
|
// inline fun withLoggingContext(
|
||||||
|
// vararg pair: Pair<LoggingContextKeys, *>,
|
||||||
|
// body: () -> Unit,
|
||||||
|
// ) =
|
||||||
|
// withLoggingContext(
|
||||||
|
// *pair
|
||||||
|
// .map {
|
||||||
|
// it.first.name to it.second.toString()
|
||||||
|
// }.toTypedArray(),
|
||||||
|
// restorePrevious = true,
|
||||||
|
// body = body,
|
||||||
|
// )
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package eventDemo.contexts.game.application.logging
|
||||||
|
|
||||||
|
enum class LoggingContextKeys {
|
||||||
|
CurrentUserId,
|
||||||
|
Notification,
|
||||||
|
Game,
|
||||||
|
Event,
|
||||||
|
Command,
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
package eventDemo.contexts.game.application.notification
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.application.notification.models.ItsTheTurnOfNotification
|
||||||
|
import eventDemo.contexts.game.application.notification.models.Notification
|
||||||
|
import eventDemo.contexts.game.application.notification.models.PilesShuffledNotification
|
||||||
|
import eventDemo.contexts.game.application.notification.models.PlayerAsJoinTheGameNotification
|
||||||
|
import eventDemo.contexts.game.application.notification.models.PlayerAsPlayACardNotification
|
||||||
|
import eventDemo.contexts.game.application.notification.models.PlayerHavePassNotification
|
||||||
|
import eventDemo.contexts.game.application.notification.models.PlayerWasReadyNotification
|
||||||
|
import eventDemo.contexts.game.application.notification.models.PlayerWinNotification
|
||||||
|
import eventDemo.contexts.game.application.notification.models.TheGameWasStartedNotification
|
||||||
|
import eventDemo.contexts.game.application.notification.models.WelcomeToTheGameNotification
|
||||||
|
import eventDemo.contexts.game.application.notification.models.YourNewCardNotification
|
||||||
|
import eventDemo.contexts.game.domain.events.CardIsPlayedEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.DrawFilledWithDiscardEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.GameCreatedEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.GameStartedEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.NewPlayerEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.PlayerActionEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.PlayerReadyEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.PlayerWinEvent
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import io.github.oshai.kotlinlogging.withLoggingContext
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
fun GameEvent.toNotification(
|
||||||
|
game: Game,
|
||||||
|
currentUserId: UserId,
|
||||||
|
): Iterable<Notification> =
|
||||||
|
Iterable {
|
||||||
|
iterator {
|
||||||
|
context(iterator: SequenceScope<Notification>)
|
||||||
|
suspend fun Notification.send() {
|
||||||
|
withLoggingContext("notification" to (this).toString()) {
|
||||||
|
logger.info { "Notification sent" }
|
||||||
|
iterator.yield(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PlayerActionEvent.isFromCurrentUser(): Boolean =
|
||||||
|
game.players.get(currentUserId).id == playerId
|
||||||
|
|
||||||
|
when (this@toNotification) {
|
||||||
|
is GameCreatedEvent -> {
|
||||||
|
// Nothing to send
|
||||||
|
}
|
||||||
|
|
||||||
|
is DrawFilledWithDiscardEvent -> {
|
||||||
|
PilesShuffledNotification().send()
|
||||||
|
}
|
||||||
|
|
||||||
|
is NewPlayerEvent -> {
|
||||||
|
if (this@toNotification.player.userId != currentUserId) {
|
||||||
|
PlayerAsJoinTheGameNotification(
|
||||||
|
player = this@toNotification.player,
|
||||||
|
).send()
|
||||||
|
} else {
|
||||||
|
WelcomeToTheGameNotification(
|
||||||
|
players = game.players,
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is CardIsPlayedEvent -> {
|
||||||
|
PlayerAsPlayACardNotification(
|
||||||
|
playerId = this@toNotification.playerId,
|
||||||
|
card = this@toNotification.card,
|
||||||
|
).send()
|
||||||
|
|
||||||
|
if (game is GameStarted) {
|
||||||
|
ItsTheTurnOfNotification(
|
||||||
|
player = game.nextPlayer,
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is GameStartedEvent -> {
|
||||||
|
TheGameWasStartedNotification(
|
||||||
|
hand =
|
||||||
|
game.players
|
||||||
|
.get(currentUserId)
|
||||||
|
.hand.cards,
|
||||||
|
).send()
|
||||||
|
|
||||||
|
if (game is GameStarted) {
|
||||||
|
ItsTheTurnOfNotification(player = game.nextPlayer)
|
||||||
|
.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerHaveDrawCardEvent -> {
|
||||||
|
if (this@toNotification.isFromCurrentUser()) {
|
||||||
|
YourNewCardNotification(
|
||||||
|
cards = this@toNotification.takenCards,
|
||||||
|
).send()
|
||||||
|
} else {
|
||||||
|
PlayerHavePassNotification(
|
||||||
|
playerId = this@toNotification.playerId,
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game is GameStarted) {
|
||||||
|
ItsTheTurnOfNotification(player = game.nextPlayer)
|
||||||
|
.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerReadyEvent -> {
|
||||||
|
PlayerWasReadyNotification(
|
||||||
|
playerId = this@toNotification.playerId,
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerWinEvent -> {
|
||||||
|
PlayerWinNotification(
|
||||||
|
playerId = this@toNotification.playerId,
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
package eventDemo.contexts.game.application.notification
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.application.command.handlers.GameCommandHandlerDispatcher
|
||||||
|
import eventDemo.contexts.game.application.command.models.GameCommand
|
||||||
|
import eventDemo.contexts.game.application.eventStores.GameRepository
|
||||||
|
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Command
|
||||||
|
import eventDemo.contexts.game.application.logging.LoggingContextKeys.CurrentUserId
|
||||||
|
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Event
|
||||||
|
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Game
|
||||||
|
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Notification
|
||||||
|
import eventDemo.contexts.game.application.logging.withLoggingContext
|
||||||
|
import eventDemo.contexts.game.application.notification.models.Notification
|
||||||
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.libs.bus.Bus
|
||||||
|
import eventDemo.libs.command.CommandUnicityChecker
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class EventToNotificationSubscriber(
|
||||||
|
private val gameEventBus: GameEventBus,
|
||||||
|
private val gameRepository: GameRepository,
|
||||||
|
) {
|
||||||
|
fun subscribeToEventsAndSendNotification(
|
||||||
|
gameId: GameId,
|
||||||
|
currentUserId: UserId,
|
||||||
|
outgoingFrameChannel: SendChannel<Notification>,
|
||||||
|
): Bus.Subscription =
|
||||||
|
withLoggingContext(CurrentUserId to currentUserId) {
|
||||||
|
gameEventBus.subscribe { event ->
|
||||||
|
val game = gameRepository.get(gameId) ?: error("Game not found")
|
||||||
|
withLoggingContext(Event to event, Game to game) {
|
||||||
|
event
|
||||||
|
.toNotification(
|
||||||
|
game = game,
|
||||||
|
currentUserId = currentUserId,
|
||||||
|
).forEach { notification ->
|
||||||
|
withLoggingContext(Notification to notification) {
|
||||||
|
outgoingFrameChannel.trySendBlocking(notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommandSubscriber(
|
||||||
|
private val gameCommandHandlerDispatcher: GameCommandHandlerDispatcher,
|
||||||
|
) {
|
||||||
|
private val controller = CommandUnicityChecker<GameCommand>()
|
||||||
|
|
||||||
|
@DelicateCoroutinesApi
|
||||||
|
fun subscribe(
|
||||||
|
currentUserId: UserId,
|
||||||
|
incomingFrameChannel: ReceiveChannel<GameCommand>,
|
||||||
|
): Job =
|
||||||
|
GlobalScope.launch {
|
||||||
|
for (command in incomingFrameChannel) {
|
||||||
|
withLoggingContext(CurrentUserId to currentUserId, Command to command) {
|
||||||
|
controller.runOnlyOnce(command) {
|
||||||
|
gameCommandHandlerDispatcher.dispatch(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.domain.notification
|
package eventDemo.contexts.game.application.notification.models
|
||||||
|
|
||||||
import eventDemo.domain.entity.Player
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
import eventDemo.configuration.serializer.UUIDSerializer
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package eventDemo.domain.notification
|
package eventDemo.contexts.game.application.notification.models
|
||||||
|
|
||||||
import eventDemo.configuration.serializer.UUIDSerializer
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package eventDemo.contexts.game.application.notification.models
|
||||||
|
|
||||||
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PilesShuffledNotification(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
) : Notification
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.domain.notification
|
package eventDemo.contexts.game.application.notification.models
|
||||||
|
|
||||||
import eventDemo.domain.entity.Player
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
import eventDemo.configuration.serializer.UUIDSerializer
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
package eventDemo.domain.notification
|
package eventDemo.contexts.game.application.notification.models
|
||||||
|
|
||||||
import eventDemo.domain.entity.Card
|
import eventDemo.contexts.game.domain.game.Card
|
||||||
import eventDemo.domain.entity.Player
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
import eventDemo.configuration.serializer.UUIDSerializer
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -10,6 +10,6 @@ import java.util.UUID
|
|||||||
data class PlayerAsPlayACardNotification(
|
data class PlayerAsPlayACardNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val player: Player,
|
val playerId: Player.PlayerId,
|
||||||
val card: Card,
|
val card: Card,
|
||||||
) : Notification
|
) : Notification
|
||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.domain.notification
|
package eventDemo.contexts.game.application.notification.models
|
||||||
|
|
||||||
import eventDemo.domain.entity.Player
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
import eventDemo.configuration.serializer.UUIDSerializer
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -9,5 +9,5 @@ import java.util.UUID
|
|||||||
data class PlayerHavePassNotification(
|
data class PlayerHavePassNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val player: Player,
|
val playerId: Player.PlayerId,
|
||||||
) : Notification
|
) : Notification
|
||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.domain.notification
|
package eventDemo.contexts.game.application.notification.models
|
||||||
|
|
||||||
import eventDemo.domain.entity.Player
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
import eventDemo.configuration.serializer.UUIDSerializer
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -9,5 +9,5 @@ import java.util.UUID
|
|||||||
data class PlayerWasReadyNotification(
|
data class PlayerWasReadyNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val player: Player,
|
val playerId: Player.PlayerId,
|
||||||
) : Notification
|
) : Notification
|
||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.domain.notification
|
package eventDemo.contexts.game.application.notification.models
|
||||||
|
|
||||||
import eventDemo.domain.entity.Player
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
import eventDemo.configuration.serializer.UUIDSerializer
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -9,5 +9,5 @@ import java.util.UUID
|
|||||||
data class PlayerWinNotification(
|
data class PlayerWinNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val player: Player,
|
val playerId: Player.PlayerId,
|
||||||
) : Notification
|
) : Notification
|
||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.domain.notification
|
package eventDemo.contexts.game.application.notification.models
|
||||||
|
|
||||||
import eventDemo.domain.entity.Card
|
import eventDemo.contexts.game.domain.game.Card
|
||||||
import eventDemo.configuration.serializer.UUIDSerializer
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -9,5 +9,5 @@ import java.util.UUID
|
|||||||
data class TheGameWasStartedNotification(
|
data class TheGameWasStartedNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val hand: List<Card>,
|
val hand: Set<Card>,
|
||||||
) : Notification
|
) : Notification
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.domain.notification
|
package eventDemo.contexts.game.application.notification.models
|
||||||
|
|
||||||
import eventDemo.domain.entity.Player
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
import eventDemo.configuration.serializer.UUIDSerializer
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.domain.notification
|
package eventDemo.contexts.game.application.notification.models
|
||||||
|
|
||||||
import eventDemo.domain.entity.Card
|
import eventDemo.contexts.game.domain.game.Card
|
||||||
import eventDemo.configuration.serializer.UUIDSerializer
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -9,5 +9,5 @@ import java.util.UUID
|
|||||||
data class YourNewCardNotification(
|
data class YourNewCardNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val card: Card,
|
val cards: Set<Card>,
|
||||||
) : Notification
|
) : Notification
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package eventDemo.contexts.game.application.ports
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
|
import eventDemo.libs.bus.Bus
|
||||||
|
|
||||||
|
interface GameEventBus : Bus<GameEvent>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package eventDemo.contexts.game.application.ports
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||||
|
|
||||||
|
interface GameEventStore : EventStore<GameEvent, GameId>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package eventDemo.domain.event.projection
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameList
|
||||||
|
|
||||||
|
interface GameListRepository {
|
||||||
|
fun getList(
|
||||||
|
limit: Int = 100,
|
||||||
|
offset: Int = 0,
|
||||||
|
): List<GameList>
|
||||||
|
|
||||||
|
fun save(gameList: GameList)
|
||||||
|
|
||||||
|
fun subscribeToBus()
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package eventDemo.contexts.game.application.ports
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameProjection
|
||||||
|
import eventDemo.libs.bus.Bus
|
||||||
|
|
||||||
|
interface GameProjectionBus : Bus<GameProjection>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package eventDemo.contexts.game.application.projections
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.events.CardIsPlayedEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.DrawFilledWithDiscardEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.GameCreatedEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.GameStartedEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.NewPlayerEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.PlayerReadyEvent
|
||||||
|
import eventDemo.contexts.game.domain.events.PlayerWinEvent
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameList
|
||||||
|
|
||||||
|
fun GameList.applyEvent(event: GameEvent): GameList =
|
||||||
|
when (event) {
|
||||||
|
is GameCreatedEvent -> {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
is NewPlayerEvent -> {
|
||||||
|
copy(
|
||||||
|
players = players + event.player,
|
||||||
|
status = GameList.Status.OPENING,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is GameStartedEvent -> {
|
||||||
|
copy(
|
||||||
|
status = GameList.Status.IS_STARTED,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerWinEvent -> {
|
||||||
|
copy(
|
||||||
|
winners = winners,
|
||||||
|
status = GameList.Status.FINISH,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is CardIsPlayedEvent -> {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerHaveDrawCardEvent -> {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerReadyEvent -> {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
is DrawFilledWithDiscardEvent -> {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package eventDemo.contexts.game.application.reaction
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.application.command.handlers.GameEventManager
|
||||||
|
import eventDemo.contexts.game.application.eventStores.GameRepository
|
||||||
|
import eventDemo.contexts.game.application.logging.LoggingContextKeys
|
||||||
|
import eventDemo.contexts.game.application.logging.withLoggingContext
|
||||||
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
||||||
|
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import java.util.concurrent.ConcurrentSkipListSet
|
||||||
|
|
||||||
|
class ReactionListener(
|
||||||
|
gameRepository: GameRepository,
|
||||||
|
private val gameEventBus: GameEventBus,
|
||||||
|
) : GameEventManager(gameRepository, gameEventBus) {
|
||||||
|
private companion object Config {
|
||||||
|
val registeredListeners = ConcurrentSkipListSet<GameEventBus>()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
fun subscribeToBus() {
|
||||||
|
if (registeredListeners.add(gameEventBus)) {
|
||||||
|
gameEventBus.subscribe { event ->
|
||||||
|
val game = event.getGame()
|
||||||
|
withLoggingContext(LoggingContextKeys.Game to game) {
|
||||||
|
sendStartGameEvent(game)
|
||||||
|
sendWinnerEvent(game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"${this::class.simpleName} is already init for this bus".let {
|
||||||
|
logger.error { it }
|
||||||
|
error(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendStartGameEvent(game: Game) {
|
||||||
|
if (game is GameCreated && game.allPlayerIsReady) {
|
||||||
|
game
|
||||||
|
.startGame()
|
||||||
|
.saveEvents()
|
||||||
|
.publishEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendWinnerEvent(game: Game) {
|
||||||
|
if (game is GameStarted && game.lastPlayerId != null) {
|
||||||
|
val lastPlayerWin =
|
||||||
|
game
|
||||||
|
.players
|
||||||
|
.get(game.lastPlayerId)
|
||||||
|
.hand.size == 0
|
||||||
|
if (lastPlayerWin) {
|
||||||
|
game
|
||||||
|
.playerWin(game.lastPlayerId)
|
||||||
|
.saveEvents()
|
||||||
|
.publishEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.Card
|
||||||
|
import eventDemo.contexts.game.domain.game.Card.Color
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
||||||
|
import eventDemo.libs.eventSource.EventId
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [GameEvent] to represent a played card.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class CardIsPlayedEvent(
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
val card: Card,
|
||||||
|
override val playerId: Player.PlayerId,
|
||||||
|
val chosenColor: Color? = null,
|
||||||
|
override val version: Int,
|
||||||
|
) : GameEvent,
|
||||||
|
PlayerActionEvent {
|
||||||
|
@Serializable(with = EventIdSerializer::class)
|
||||||
|
override val eventId: EventId = EventId(UUID.randomUUID())
|
||||||
|
override val createdAt: Instant = Clock.System.now()
|
||||||
|
|
||||||
|
val theColorCard get() = if (card is Card.CardWithColor) card.color else chosenColor
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.DiscardPile
|
||||||
|
import eventDemo.contexts.game.domain.game.DrawPile
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
||||||
|
import eventDemo.libs.eventSource.EventId
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the Pile are shuffled after the draw pille was empty
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
class DrawFilledWithDiscardEvent(
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
val newDrawPile: DrawPile,
|
||||||
|
val newDiscardPile: DiscardPile,
|
||||||
|
override val version: Int,
|
||||||
|
) : GameEvent {
|
||||||
|
@Serializable(with = EventIdSerializer::class)
|
||||||
|
override val eventId: EventId = EventId(UUID.randomUUID())
|
||||||
|
override val createdAt: Instant = Clock.System.now()
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
||||||
|
import eventDemo.libs.eventSource.EventId
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This [GameEvent] is sent when all players are ready.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class GameCreatedEvent(
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
override val version: Int,
|
||||||
|
) : GameEvent {
|
||||||
|
@Serializable(with = EventIdSerializer::class)
|
||||||
|
override val eventId: EventId = EventId(UUID.randomUUID())
|
||||||
|
override val createdAt: Instant = Clock.System.now()
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
|
||||||
|
import eventDemo.libs.eventSource.Event
|
||||||
|
import eventDemo.libs.eventSource.EventId
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [Event] of a Game.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
sealed interface GameEvent : Event<GameId> {
|
||||||
|
@Serializable(with = EventIdSerializer::class)
|
||||||
|
override val eventId: EventId
|
||||||
|
|
||||||
|
@Serializable(with = GameIdSerializer::class)
|
||||||
|
override val aggregateId: GameId
|
||||||
|
override val version: Int
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.DiscardPile
|
||||||
|
import eventDemo.contexts.game.domain.game.DrawPile
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
|
import eventDemo.contexts.game.domain.game.PlayerHand
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
|
||||||
|
import eventDemo.libs.eventSource.EventId
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This [GameEvent] is sent when all players are ready.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class GameStartedEvent(
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
@Serializable(with = PlayerIdSerializer::class)
|
||||||
|
val firstPlayer: Player.PlayerId,
|
||||||
|
val playersHans: Map<Player.PlayerId, PlayerHand>,
|
||||||
|
val drawPile: DrawPile,
|
||||||
|
val discardPile: DiscardPile,
|
||||||
|
override val version: Int,
|
||||||
|
) : GameEvent {
|
||||||
|
@Serializable(with = EventIdSerializer::class)
|
||||||
|
override val eventId: EventId = EventId(UUID.randomUUID())
|
||||||
|
override val createdAt: Instant = Clock.System.now()
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
||||||
|
import eventDemo.libs.eventSource.EventId
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [GameEvent] to represent a new player joining the game.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class NewPlayerEvent(
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
val player: Player,
|
||||||
|
override val version: Int,
|
||||||
|
) : GameEvent,
|
||||||
|
PlayerActionEvent {
|
||||||
|
override val playerId: Player.PlayerId get() = player.id
|
||||||
|
|
||||||
|
@Serializable(with = EventIdSerializer::class)
|
||||||
|
override val eventId: EventId = EventId(UUID.randomUUID())
|
||||||
|
override val createdAt: Instant = Clock.System.now()
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface PlayerActionEvent : GameEvent {
|
||||||
|
val playerId: Player.PlayerId
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.Card
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
|
||||||
|
import eventDemo.libs.eventSource.EventId
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This [GameEvent] is sent when a player can play.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class PlayerHaveDrawCardEvent(
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
@Serializable(with = PlayerIdSerializer::class)
|
||||||
|
override val playerId: Player.PlayerId,
|
||||||
|
val takenCards: Set<Card>,
|
||||||
|
override val version: Int,
|
||||||
|
) : GameEvent,
|
||||||
|
PlayerActionEvent {
|
||||||
|
@Serializable(with = EventIdSerializer::class)
|
||||||
|
override val eventId: EventId = EventId(UUID.randomUUID())
|
||||||
|
override val createdAt: Instant = Clock.System.now()
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
|
||||||
|
import eventDemo.libs.eventSource.EventId
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This [GameEvent] is sent when a player is ready.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class PlayerReadyEvent(
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
@Serializable(with = PlayerIdSerializer::class)
|
||||||
|
override val playerId: Player.PlayerId,
|
||||||
|
override val version: Int,
|
||||||
|
) : GameEvent,
|
||||||
|
PlayerActionEvent {
|
||||||
|
@Serializable(with = EventIdSerializer::class)
|
||||||
|
override val eventId: EventId = EventId(UUID.randomUUID())
|
||||||
|
override val createdAt: Instant = Clock.System.now()
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
|
import eventDemo.contexts.game.domain.game.GameId
|
||||||
|
import eventDemo.contexts.game.domain.game.Player
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
||||||
|
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
|
||||||
|
import eventDemo.libs.eventSource.EventId
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This [GameEvent] is sent when a player is ready.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class PlayerWinEvent(
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
@Serializable(with = PlayerIdSerializer::class)
|
||||||
|
override val playerId: Player.PlayerId,
|
||||||
|
override val version: Int,
|
||||||
|
) : GameEvent,
|
||||||
|
PlayerActionEvent {
|
||||||
|
@Serializable(with = EventIdSerializer::class)
|
||||||
|
override val eventId: EventId = EventId(UUID.randomUUID())
|
||||||
|
override val createdAt: Instant = Clock.System.now()
|
||||||
|
}
|
||||||
+31
-13
@@ -1,6 +1,6 @@
|
|||||||
package eventDemo.domain.entity
|
package eventDemo.contexts.game.domain.game
|
||||||
|
|
||||||
import eventDemo.configuration.serializer.UUIDSerializer
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -23,7 +23,7 @@ sealed interface Card {
|
|||||||
Green,
|
Green,
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ColorCard : Card {
|
sealed interface CardWithColor : Card {
|
||||||
val color: Color
|
val color: Color
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,11 +38,14 @@ sealed interface Card {
|
|||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Card,
|
) : Card,
|
||||||
ColorCard {
|
CardWithColor {
|
||||||
init {
|
init {
|
||||||
if (number > 9) error("Card number cannot be greater of 9")
|
if (number > 9) error("Card number cannot be greater of 9")
|
||||||
if (number < 0) error("Card number cannot be lower of 0")
|
if (number < 0) error("Card number cannot be lower of 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
"Numeric Card $number $color"
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Special : Card
|
sealed interface Special : Card
|
||||||
@@ -57,7 +60,10 @@ sealed interface Card {
|
|||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Special,
|
) : Special,
|
||||||
ColorCard
|
CardWithColor {
|
||||||
|
override fun toString(): String =
|
||||||
|
"Revert Card $color"
|
||||||
|
}
|
||||||
|
|
||||||
sealed interface PassTurnCard : Card
|
sealed interface PassTurnCard : Card
|
||||||
|
|
||||||
@@ -71,8 +77,11 @@ sealed interface Card {
|
|||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Special,
|
) : Special,
|
||||||
ColorCard,
|
CardWithColor,
|
||||||
PassTurnCard
|
PassTurnCard {
|
||||||
|
override fun toString(): String =
|
||||||
|
"Pass Card $color"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A play card to force the next player to take 2 card and pass the turn.
|
* A play card to force the next player to take 2 card and pass the turn.
|
||||||
@@ -84,10 +93,13 @@ sealed interface Card {
|
|||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Special,
|
) : Special,
|
||||||
ColorCard,
|
CardWithColor,
|
||||||
PassTurnCard
|
PassTurnCard {
|
||||||
|
override fun toString(): String =
|
||||||
|
"Plus2 Card $color"
|
||||||
|
}
|
||||||
|
|
||||||
sealed interface AllColorCard : Card
|
sealed interface CardWith4Color : Card
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A play card to force the next player to take 4 card and pass the turn.
|
* A play card to force the next player to take 4 card and pass the turn.
|
||||||
@@ -98,8 +110,11 @@ sealed interface Card {
|
|||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Special,
|
) : Special,
|
||||||
AllColorCard,
|
CardWith4Color,
|
||||||
PassTurnCard
|
PassTurnCard {
|
||||||
|
override fun toString(): String =
|
||||||
|
"Plus4 Card"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A play card to change the color.
|
* A play card to change the color.
|
||||||
@@ -110,5 +125,8 @@ sealed interface Card {
|
|||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Special,
|
) : Special,
|
||||||
AllColorCard
|
CardWith4Color {
|
||||||
|
override fun toString(): String =
|
||||||
|
"Change color Card"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package eventDemo.contexts.game.domain.game
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
@Serializable
|
||||||
|
value class DiscardPile(
|
||||||
|
val cards: Set<Card> = emptySet(),
|
||||||
|
) {
|
||||||
|
fun withNewCard(card: Card): DiscardPile =
|
||||||
|
DiscardPile(cards + card)
|
||||||
|
|
||||||
|
val topCard: Card? get() = cards.lastOrNull()
|
||||||
|
|
||||||
|
val topCardColor: Card.Color? get() = topCard?.let { if (it is Card.CardWithColor) it.color else null }
|
||||||
|
|
||||||
|
val size: Int get() = cards.size
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package eventDemo.contexts.game.domain.game
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
@Serializable
|
||||||
|
value class DrawPile(
|
||||||
|
val cards: Set<Card> = emptySet(),
|
||||||
|
) {
|
||||||
|
val size: Int get() = cards.size
|
||||||
|
|
||||||
|
fun take(number: Int): Pair<DrawPile, Set<Card>> =
|
||||||
|
cards.drop(number).toDrawPile() to cards.take(number).toSet()
|
||||||
|
|
||||||
|
val remainingCards
|
||||||
|
get() = cards.size
|
||||||
|
|
||||||
|
fun shuffled(): DrawPile =
|
||||||
|
cards.shuffled().toDrawPile()
|
||||||
|
|
||||||
|
val firstCard get() = cards.first()
|
||||||
|
|
||||||
|
fun generateValidDrawPile(): DrawPile =
|
||||||
|
if (cards.first() is Card.CardWith4Color) {
|
||||||
|
DrawPile(setOf(cards.first()) + cards.drop(1))
|
||||||
|
.generateValidDrawPile()
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<Card>.toDrawPile(): DrawPile =
|
||||||
|
DrawPile(this.toSet())
|
||||||
+5
-4
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.domain.entity
|
package eventDemo.contexts.game.domain.game
|
||||||
|
|
||||||
import eventDemo.configuration.serializer.GameIdSerializer
|
import eventDemo.libs.eventSource.AggregateId
|
||||||
import eventDemo.libs.event.AggregateId
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -9,8 +9,9 @@ import java.util.UUID
|
|||||||
* An [AggregateId] for a game.
|
* An [AggregateId] for a game.
|
||||||
*/
|
*/
|
||||||
@JvmInline
|
@JvmInline
|
||||||
@Serializable(with = GameIdSerializer::class)
|
@Serializable
|
||||||
value class GameId(
|
value class GameId(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : AggregateId {
|
) : AggregateId {
|
||||||
override fun toString(): String =
|
override fun toString(): String =
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package eventDemo.contexts.game.domain.game
|
||||||
|
|
||||||
|
import eventDemo.libs.eventSource.AggregateId
|
||||||
|
import eventDemo.libs.helpers.withReplacedValue
|
||||||
|
import eventDemo.libs.serializer.UUIDSerializer
|
||||||
|
import eventDemo.sharedKernel.UserId
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Player(
|
||||||
|
val name: String,
|
||||||
|
val userId: UserId,
|
||||||
|
val hand: PlayerHand = PlayerHand(),
|
||||||
|
val id: PlayerId = PlayerId(UUID.randomUUID()),
|
||||||
|
) {
|
||||||
|
@JvmInline
|
||||||
|
@Serializable
|
||||||
|
value class PlayerId(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
) : AggregateId {
|
||||||
|
override fun toString(): String =
|
||||||
|
id.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PlayerList(
|
||||||
|
val players: Set<Player> = emptySet(),
|
||||||
|
) : Set<Player> by players {
|
||||||
|
fun get(id: Player.PlayerId): Player =
|
||||||
|
find { it.id == id } ?: error("no player with id $id")
|
||||||
|
|
||||||
|
fun get(id: UserId): Player =
|
||||||
|
find { it.userId == id } ?: error("no player with userId $id")
|
||||||
|
|
||||||
|
fun withNewCardOnPlayerHand(
|
||||||
|
playerId: Player.PlayerId,
|
||||||
|
cards: Set<Card>,
|
||||||
|
): PlayerList =
|
||||||
|
players
|
||||||
|
.withReplacedValue(get(playerId)) {
|
||||||
|
it.copy(
|
||||||
|
hand = it.hand.withNewCards(cards),
|
||||||
|
)
|
||||||
|
}.let { PlayerList(it) }
|
||||||
|
|
||||||
|
fun withDropCardOnPlayerHand(
|
||||||
|
playerId: Player.PlayerId,
|
||||||
|
card: Card,
|
||||||
|
): PlayerList =
|
||||||
|
players
|
||||||
|
.withReplacedValue(get(playerId)) {
|
||||||
|
it.copy(
|
||||||
|
hand = it.hand.withoutTheCards(setOf(card)),
|
||||||
|
)
|
||||||
|
}.let { PlayerList(it) }
|
||||||
|
|
||||||
|
operator fun plus(player: Player): PlayerList =
|
||||||
|
PlayerList(players + player)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user