Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
774e80d9d5
|
||
|
|
505cfe38f0
|
||
|
|
85bcdcadb8
|
||
|
|
e67c475c38
|
||
|
|
07983dc5b0
|
||
|
|
5e9587b93f
|
||
|
|
fd62f2574d
|
||
|
|
828bd0639e
|
||
|
|
da13b0d2b8
|
||
|
|
d632dc0f6b
|
||
|
|
b60e4aa457
|
||
|
|
e262f35b27
|
||
|
|
77e4cab8ad
|
||
|
|
7291419c9f
|
||
|
|
b2b8fcf92f
|
||
|
|
4e4b307275
|
||
|
|
e87a36caa5
|
||
|
|
e2d7942c7e
|
||
|
|
b313b39cf4
|
@@ -0,0 +1,26 @@
|
|||||||
|
# Version control
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
|
||||||
|
# Gradle build outputs / caches (must always be rebuilt fresh inside the image)
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
.kotlin
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.run
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
# Docker-only local files (secrets/env must never be baked into the image)
|
||||||
|
docker/.env
|
||||||
|
docker/*.env.docker
|
||||||
|
docker/*.secret
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.hprof
|
||||||
|
.gradle-docker-cache/
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
* text=auto
|
||||||
|
* eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.png binary
|
||||||
|
*.jar binary
|
||||||
|
gradlew.bat eol=crlf
|
||||||
|
gradlew text eol=lf
|
||||||
+49
-33
@@ -18,10 +18,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
run: echo "key=gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}" >> $GITHUB_OUTPUT
|
run: echo "key=gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Cache Gradle dependencies
|
- name: Cache Gradle dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -48,16 +48,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|
||||||
- name: Restore Gradle cache
|
- name: Restore Gradle cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -70,58 +70,74 @@ jobs:
|
|||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: ./gradlew ktlintCheck
|
# Path scoped to :backend on purpose: :composeApp applies the Android Gradle plugin,
|
||||||
|
# which needs an Android SDK to even configure. Keeping every gradlew invocation on a
|
||||||
|
# fully-qualified project path (with org.gradle.configureondemand=true) lets CI skip
|
||||||
|
# configuring :composeApp entirely, so no Android SDK setup is needed on this runner.
|
||||||
|
run: ./gradlew :backend:ktlintCheck
|
||||||
|
|
||||||
- name: Publish ktlint report
|
- name: Publish ktlint report
|
||||||
uses: yutailang0119/action-ktlint@v4
|
uses: yutailang0119/action-ktlint@v5
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
report-path: build/reports/ktlint/**/*.xml
|
report-path: backend/build/reports/ktlint/**/*.xml
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
|
|
||||||
test:
|
test:
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GRADLE_CACHE_DIR: ${{ github.workspace }}/.gradle-docker-cache
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Install a pinned Docker Compose version
|
||||||
uses: actions/setup-java@v4
|
run: |
|
||||||
with:
|
mkdir -p ~/.docker/cli-plugins
|
||||||
distribution: 'temurin'
|
curl -fSL https://github.com/docker/compose/releases/download/v5.1.4/docker-compose-linux-x86_64 \
|
||||||
java-version: '21'
|
-o ~/.docker/cli-plugins/docker-compose
|
||||||
|
chmod +x ~/.docker/cli-plugins/docker-compose
|
||||||
|
docker compose version
|
||||||
|
|
||||||
- name: Restore Gradle cache
|
- name: Prepare docker secrets
|
||||||
uses: actions/cache@v3
|
run: |
|
||||||
|
[ -f docker/postgresql.secret ] || echo -n "changeit" > docker/postgresql.secret
|
||||||
|
|
||||||
|
- name: Generate cache key
|
||||||
|
id: cache-key-generator
|
||||||
|
run: echo "key=gradle-docker-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Restore Gradle cache (Docker)
|
||||||
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: |
|
path: ${{ env.GRADLE_CACHE_DIR }}
|
||||||
~/.gradle/caches
|
key: ${{ steps.cache-key-generator.outputs.key }}
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ needs.build.outputs.cache-key }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
gradle-${{ runner.os }}-
|
gradle-docker-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Grant execute permission to Gradle wrapper
|
- name: Prepare cache directory permissions
|
||||||
run: chmod +x gradlew
|
run: |
|
||||||
|
mkdir -p "$GRADLE_CACHE_DIR"
|
||||||
|
chmod -R 777 "$GRADLE_CACHE_DIR"
|
||||||
|
|
||||||
- name: Start CI Docker Compose services
|
- name: Run tests in Docker
|
||||||
run: ./gradlew composeUp -Pci
|
run: docker compose -f docker/docker-compose-test.yaml run tests
|
||||||
|
|
||||||
- name: Run tests
|
- name: Shut down Docker services
|
||||||
run: ./gradlew test -x composeUp --no-daemon
|
if: always()
|
||||||
|
run: docker compose -f docker/docker-compose-test.yaml down -v
|
||||||
|
|
||||||
- name: Upload test reports
|
- name: Upload test reports
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: test-results
|
name: test-results
|
||||||
path: build/reports/tests/test
|
path: backend/build/reports/tests/test
|
||||||
|
|
||||||
- name: Publish Test Report
|
- name: Publish Test Report
|
||||||
uses: dorny/test-reporter@v1
|
uses: dorny/test-reporter@v3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: JUnit Tests
|
name: JUnit Tests
|
||||||
path: build/test-results/test/TEST-*.xml
|
path: backend/build/test-results/test/TEST-*.xml
|
||||||
reporter: java-junit
|
reporter: java-junit
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
.gradle
|
.gradle
|
||||||
|
.kotlin/
|
||||||
build/
|
build/
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
!**/src/main/**/build/
|
!**/src/main/**/build/
|
||||||
!**/src/test/**/build/
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### Android / KMP ###
|
||||||
|
local.properties
|
||||||
|
captures/
|
||||||
|
.cxx/
|
||||||
|
|
||||||
### STS ###
|
### STS ###
|
||||||
.apt_generated
|
.apt_generated
|
||||||
.classpath
|
.classpath
|
||||||
@@ -37,3 +43,4 @@ out/
|
|||||||
/docker/.env
|
/docker/.env
|
||||||
/docker/*.secret
|
/docker/*.secret
|
||||||
*.hprof
|
*.hprof
|
||||||
|
/.gradle-docker-cache/
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="docker composeUp" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value="composeUp" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>false</RunAsTest>
|
||||||
|
<GradleProfilingDisabled>false</GradleProfilingDisabled>
|
||||||
|
<GradleCoverageDisabled>false</GradleCoverageDisabled>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# CLAUDE.md — event-demo
|
||||||
|
|
||||||
|
Ce fichier donne le contexte du projet pour toute session Claude Code future sur ce dépôt.
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
`event-demo` est un projet démo personnel (Fabrice Lecomte) qui illustre plusieurs patterns
|
||||||
|
d'architecture backend :
|
||||||
|
|
||||||
|
- Event Sourcing
|
||||||
|
- Event-Driven (bus d'événements asynchrone)
|
||||||
|
- CQRS (séparation commandes / projections en lecture)
|
||||||
|
- Architecture Hexagonale (ports & adapters), un dossier par *bounded context*
|
||||||
|
|
||||||
|
Le cas d'usage servant de support est un jeu de cartes façon UNO (créer une partie, rejoindre,
|
||||||
|
jouer une carte, piocher, etc.), avec authentification des joueurs.
|
||||||
|
|
||||||
|
Dépôts distants configurés : `gitea` (auto-hébergé, git.gogn.synology.me — remote historique)
|
||||||
|
et `github` (`flecomte/event-demo`, miroir). Vérifier vers lequel pousser selon le contexte.
|
||||||
|
|
||||||
|
## Stack technique
|
||||||
|
|
||||||
|
- **Langage** : Kotlin 2.1.21, JDK 21 (toolchain Gradle)
|
||||||
|
- **Framework serveur** : Ktor 3.5.1 (Netty), DI via Koin 4.2.1
|
||||||
|
- **Sérialisation** : kotlinx.serialization (JSON)
|
||||||
|
- **Persistance** :
|
||||||
|
- PostgreSQL (event store, via HikariCP) + migrations Flyway (`migrations/events/`)
|
||||||
|
- RabbitMQ (bus d'événements / bus de commandes, via amqp-client)
|
||||||
|
- **Auth** : JWT (ktor-server-auth-jwt), hash de mot de passe via password4j
|
||||||
|
- **Infra dev/prod** : Docker Compose (fichiers `docker/docker-compose-{dev,test,prod}.yaml`
|
||||||
|
incluant des « parts » réutilisables dans `docker/parts/`), reverse proxy Træfik
|
||||||
|
- **Tests** : Kotest (runner JUnit5), MockK, kotest-extensions-koin, ArchUnit (test d'architecture)
|
||||||
|
- **Qualité** : ktlint (`ktlint_official`, standard + experimental activés), reporting checkstyle
|
||||||
|
- **CI** : GitHub Actions (`.github/workflows/tests.yml`) — build/cache Gradle, `ktlintCheck`,
|
||||||
|
puis tests exécutés **dans Docker** (`docker compose -f docker/docker-compose-test.yaml run tests`)
|
||||||
|
- **API** : documentée en OpenAPI (`resources/openapi/documentation.yaml`)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Un dossier par *bounded context* sous `src/main/kotlin/eventDemo/contexts/<context>/`, chacun
|
||||||
|
strictement découpé en 3 couches :
|
||||||
|
|
||||||
|
- `domain/` — aucune dépendance vers les autres couches
|
||||||
|
- `application/` — ne dépend que de `domain`
|
||||||
|
- `infrastructure/` — dépend de `domain` et `application`
|
||||||
|
|
||||||
|
Contexts actuels :
|
||||||
|
|
||||||
|
- **`auth`** : `User`, création de compte, login JWT, event store dédié (Postgresql),
|
||||||
|
projection utilisateur.
|
||||||
|
- **`game`** : cœur du jeu — `Card`, `DrawPile`/`DiscardPile`, `Player`, `GameId`, commandes
|
||||||
|
(`JoinTheGameCommand`, `PlayCardCommand`, `ReadyToPlayCommand`, `TakeCartFromDrawPileCommand`),
|
||||||
|
state machine du jeu via `sealed interface Game` (`GameInit` → `GameCreated` → `GameStarted` →
|
||||||
|
`GameEnded`), notifications, projections (liste de parties), listeners/réactions.
|
||||||
|
|
||||||
|
Libs transverses dans `libs/` (indépendantes de tout contexte) :
|
||||||
|
|
||||||
|
- `bus/` — abstraction `Bus<E>` avec implémentations in-memory et RabbitMQ (fanout exchange)
|
||||||
|
- `command/` — `Command`, `CommandUnicityChecker` (empêche la double exécution d'une commande,
|
||||||
|
cache glissant de 10 min par défaut)
|
||||||
|
- `eventSource/` — `Event`, `EventStream` (append/lecture par version, gestion de
|
||||||
|
`VersionConflictException`), `EventStore` in-memory / Postgresql
|
||||||
|
- `helpers/`, `serializer/` — utilitaires (conversion de frames WebSocket, sérialiseurs UUID, etc.)
|
||||||
|
|
||||||
|
## Patterns notables dans le code
|
||||||
|
|
||||||
|
- **Event sourcing** : `Game.loadFromHistory(events)` reconstruit l'état en repliant
|
||||||
|
(`fold`) les événements sur une state machine scellée, en utilisant la syntaxe Kotlin 2.1
|
||||||
|
`when` avec garde `if` (ex. `is GameCreatedEvent if this is GameInit -> applyEvent(event)`).
|
||||||
|
- **CQRS** : écriture via les command handlers (`application/command/handlers`), lecture via des
|
||||||
|
projections dédiées (`application/projections`), propagées via le bus RabbitMQ, pas de couplage
|
||||||
|
direct avec l'écriture.
|
||||||
|
- **Event-driven** : réactions asynchrones (`ReactionListener`, `EventToNotificationSubscriber`)
|
||||||
|
déclenchées par le bus RabbitMQ (exchange fanout, une queue par abonné).
|
||||||
|
- **Exceptions métier** : hiérarchie `GameException` / `IllegalActionException` dans
|
||||||
|
`domain/game/errors`, une exception par règle métier violée (ex.
|
||||||
|
`NeedMorePlayersToStartGameException`, `ItsNotTheTurnException`).
|
||||||
|
|
||||||
|
## Commandes utiles
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./gradlew build # build complet
|
||||||
|
./gradlew test # tests (JUnit5 via Kotest)
|
||||||
|
./gradlew ktlintCheck # lint
|
||||||
|
./gradlew ktlintFormat # auto-format
|
||||||
|
./gradlew buildFatJar # jar exécutable "all-in-one" (utilisé par le Dockerfile prod)
|
||||||
|
|
||||||
|
# Dépendances seules (Postgres, RabbitMQ, Træfik, pgAdmin...) pour lancer l'app en local hors docker
|
||||||
|
docker compose -f docker/docker-compose-dev.yaml up -d
|
||||||
|
|
||||||
|
# Stack de test façon CI
|
||||||
|
docker compose -f docker/docker-compose-test.yaml up -d
|
||||||
|
# ou directement (comme en CI) :
|
||||||
|
docker compose -f docker/docker-compose-test.yaml run tests
|
||||||
|
|
||||||
|
# Stack complète en prod
|
||||||
|
docker compose -f docker/docker-compose-prod.yaml -p event-demo up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
URLs en dev (voir `doc/installation.md`, nécessite Træfik + résolution des `*.traefik.me`) :
|
||||||
|
API sur `http://api.traefik.me/`, dashboard
|
||||||
|
Træfik, pgAdmin et RabbitMQ management exposés via des sous-domaines `traefik.me`.
|
||||||
|
|
||||||
|
## Conventions de code
|
||||||
|
|
||||||
|
- ktlint en mode `ktlint_official` + règles `standard` et `experimental` activées
|
||||||
|
(voir `.editorconfig`), indentation **2 espaces**, virgules finales (*trailing commas*)
|
||||||
|
systématiques, wrapping forcé des expressions/signatures multi-lignes.
|
||||||
|
- Fins de ligne forcées en **LF** (`.gitattributes`), sauf `gradlew.bat` en CRLF.
|
||||||
|
- Code et identifiants en anglais.
|
||||||
|
- Style Kotlin idiomatique/fonctionnel : `fold`, `let`, `apply`, `when` exhaustifs, classes/interfaces
|
||||||
|
scellées (`sealed class`/`sealed interface`) pour modéliser états et événements plutôt que des enums
|
||||||
|
avec des champs optionnels.
|
||||||
|
|
||||||
|
## Pièges connus / choses à savoir avant de toucher au build ou à la CI
|
||||||
|
|
||||||
|
- **MockK/ByteBuddy en Docker** : l'auto-attach dynamique de MockK échoue dans les conteneurs
|
||||||
|
(le handshake SIGQUIT de l'AttachListener JVM time-out). Le `build.gradle.kts` charge donc
|
||||||
|
l'agent `byte-buddy-agent` de façon statique via `-javaagent` pour les tâches `Test`, afin
|
||||||
|
que MockK détecte l'instrumentation déjà présente et saute l'attach dynamique. Ne pas retirer
|
||||||
|
ce bloc sans repenser l'exécution des tests en Docker.
|
||||||
|
- **Secret Postgres en CI** : `docker/postgresql.secret` est généré à la volée par le workflow
|
||||||
|
GitHub Actions s'il n'existe pas (`echo -n "changeit" > docker/postgresql.secret`) — normal,
|
||||||
|
pas un fichier à committer.
|
||||||
|
- Les tests « officiels » de la CI tournent **dans Docker**, pas directement via `./gradlew test`
|
||||||
|
sur l'hôte — en cas de comportement différent entre local et CI, vérifier d'abord les
|
||||||
|
variables d'environnement/versions du `docker-compose-test.yaml`.
|
||||||
|
|
||||||
|
## Historique récent (pour contexte)
|
||||||
|
|
||||||
|
Le projet a connu un « Massive refactor to build the V2 » (commit `e2d7942`) : passage d'une
|
||||||
|
architecture par couches techniques plates (`adapter/presenter/domain`) à l'organisation actuelle
|
||||||
|
par bounded context (`auth`/`game`) avec 3 couches hexagonales chacune.
|
||||||
@@ -2,7 +2,6 @@ Event Demo
|
|||||||
==========
|
==========
|
||||||
- [Installation](./doc/installation.md)
|
- [Installation](./doc/installation.md)
|
||||||
- [What's the demo for ?](#whats-the-demo-for-)
|
- [What's the demo for ?](#whats-the-demo-for-)
|
||||||
- [What's in this demo](#whats-in-this-demo)
|
|
||||||
- [The stack](#the-stack)
|
- [The stack](#the-stack)
|
||||||
- [Architecture](./doc/architecture.md)
|
- [Architecture](./doc/architecture.md)
|
||||||
|
|
||||||
@@ -18,31 +17,6 @@ of different patterns and architectures.
|
|||||||
- The CQRS pattern.
|
- The CQRS pattern.
|
||||||
- With the Hexagonal architecture.
|
- With the Hexagonal architecture.
|
||||||
|
|
||||||
|
|
||||||
What's in this demo
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
- The **event sourcing** pattern.
|
|
||||||
- The **event driven** pattern.
|
|
||||||
- The **CQRS** pattern with **command** and **query**.
|
|
||||||
- A fully **asynchronous** architecture.Concurently process.
|
|
||||||
- A **pure Kotlin** implementation of **readmodel**/**projection**.
|
|
||||||
- A **Redis** implementation of **readmodel**/**projection**.
|
|
||||||
- A **pure Kotlin** implementation of **Event Store**.
|
|
||||||
- A **Postgresql** implementation of **Event Store**.
|
|
||||||
- A **pure Kotlin** implementation of **Event Bus**.
|
|
||||||
- A **RabbitMQ** implementation of **Event Bus**.
|
|
||||||
- A **Hexagonal** architecture.
|
|
||||||
- Use of **Web Sockets**.
|
|
||||||
- Use of the classic **Rest** route.
|
|
||||||
- Simple usage of the **JWT**.
|
|
||||||
- The **Ktor** framework.
|
|
||||||
- The **Koin** Dependency Injection framework
|
|
||||||
- Concurrently process.
|
|
||||||
- Use of coroutines.
|
|
||||||
- Using **docker compose** for the stack with **traefik**.
|
|
||||||
- Use of **flyway** to migrate the postgresql schema.
|
|
||||||
|
|
||||||
The stack
|
The stack
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -51,11 +25,11 @@ Language
|
|||||||
|
|
||||||
Framework
|
Framework
|
||||||
- Ktor
|
- Ktor
|
||||||
|
- with Koin for Dependency Injection
|
||||||
|
|
||||||
Database
|
Database
|
||||||
- Postgresql
|
- Postgresql
|
||||||
- with Flyway
|
- with Flyway
|
||||||
- Redis
|
|
||||||
- RabbitMQ
|
- RabbitMQ
|
||||||
|
|
||||||
Infra
|
Infra
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
||||||
|
|
||||||
|
val ktorVersion: Provider<String> = providers.gradleProperty("ktor_version")
|
||||||
|
val kotlinVersion: Provider<String> = providers.gradleProperty("kotlin_version")
|
||||||
|
val kotlinSerializationVersion: Provider<String> = providers.gradleProperty("kotlin_serialization_version")
|
||||||
|
val logbackVersion: Provider<String> = providers.gradleProperty("logback_version")
|
||||||
|
val koinVersion: Provider<String> = providers.gradleProperty("koin_version")
|
||||||
|
val kotlinLoggingVersion: Provider<String> = providers.gradleProperty("kotlin_logging_version")
|
||||||
|
val kotestVersion: Provider<String> = providers.gradleProperty("kotest_version")
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
application
|
||||||
|
kotlin("jvm")
|
||||||
|
id("io.ktor.plugin") version "3.5.1"
|
||||||
|
id("org.jetbrains.kotlin.plugin.serialization")
|
||||||
|
id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "io.github.flecomte"
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("eventDemo.ApplicationKt")
|
||||||
|
|
||||||
|
val isDevelopment: Boolean = project.ext.has("development")
|
||||||
|
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
|
||||||
|
}
|
||||||
|
|
||||||
|
configure<KtlintExtension> {
|
||||||
|
version.set("1.8.0")
|
||||||
|
}
|
||||||
|
ktlint {
|
||||||
|
reporters {
|
||||||
|
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.add("-opt-in=kotlin.uuid.ExperimentalUuidApi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test>().configureEach {
|
||||||
|
useJUnitPlatform()
|
||||||
|
jvmArgs("-Djdk.attach.allowAttachSelf=true", "-XX:+EnableDynamicAgentLoading")
|
||||||
|
// Dynamic self-attach (used by MockK/ByteBuddy) times out in Docker containers because the
|
||||||
|
// SIGQUIT-triggered AttachListener handshake never completes there. Loading the byte-buddy
|
||||||
|
// agent jar statically via -javaagent avoids the attach handshake entirely: MockK detects the
|
||||||
|
// pre-installed Instrumentation instance and skips dynamic attach.
|
||||||
|
doFirst {
|
||||||
|
val agentJar =
|
||||||
|
classpath.files.firstOrNull { it.name.startsWith("byte-buddy-agent") }
|
||||||
|
?: error("byte-buddy-agent jar not found on test classpath")
|
||||||
|
jvmArgs("-javaagent:$agentJar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":shared"))
|
||||||
|
implementation("io.ktor:ktor-server-core-jvm")
|
||||||
|
implementation("io.ktor:ktor-server-auth-jvm")
|
||||||
|
implementation("io.ktor:ktor-server-auth-jwt-jvm")
|
||||||
|
implementation("io.ktor:ktor-server-auto-head-response-jvm")
|
||||||
|
implementation("io.ktor:ktor-server-resources")
|
||||||
|
implementation("io.ktor:ktor-server-content-negotiation-jvm")
|
||||||
|
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
|
||||||
|
implementation("io.ktor:ktor-server-websockets-jvm")
|
||||||
|
implementation("io.ktor:ktor-server-cors-jvm")
|
||||||
|
implementation("io.ktor:ktor-server-host-common-jvm")
|
||||||
|
implementation("io.ktor:ktor-server-status-pages-jvm")
|
||||||
|
implementation("io.ktor:ktor-server-netty-jvm")
|
||||||
|
implementation("io.ktor:ktor-server-data-conversion")
|
||||||
|
implementation("io.ktor:ktor-client-content-negotiation")
|
||||||
|
implementation("io.ktor:ktor-client-auth")
|
||||||
|
implementation("ch.qos.logback:logback-classic:${logbackVersion.get()}")
|
||||||
|
implementation("io.insert-koin:koin-ktor:${koinVersion.get()}")
|
||||||
|
implementation("io.insert-koin:koin-logger-slf4j:${koinVersion.get()}")
|
||||||
|
implementation("io.github.oshai:kotlin-logging-jvm:${kotlinLoggingVersion.get()}")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:${kotlinSerializationVersion.get()}")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
|
||||||
|
implementation("org.postgresql:postgresql:42.7.13")
|
||||||
|
implementation("com.zaxxer:HikariCP:6.3.0")
|
||||||
|
implementation("com.rabbitmq:amqp-client:5.25.0")
|
||||||
|
implementation("com.password4j:password4j:1.8.4")
|
||||||
|
|
||||||
|
// Force version of sub library (for security)
|
||||||
|
implementation("commons-codec:commons-codec:1.13")
|
||||||
|
|
||||||
|
testImplementation("io.kotest:kotest-extensions-koin:${kotestVersion.get()}")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion.get()}")
|
||||||
|
testImplementation("io.ktor:ktor-server-test-host-jvm:${ktorVersion.get()}")
|
||||||
|
testImplementation("io.kotest:kotest-runner-junit5:${kotestVersion.get()}")
|
||||||
|
testImplementation("io.mockk:mockk:1.14.11")
|
||||||
|
testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")
|
||||||
|
}
|
||||||
-2
@@ -3,7 +3,6 @@ package eventDemo.configuration
|
|||||||
import io.ktor.server.config.ApplicationConfig
|
import io.ktor.server.config.ApplicationConfig
|
||||||
|
|
||||||
data class Configuration(
|
data class Configuration(
|
||||||
val redisUrl: String,
|
|
||||||
val jwtSecret: String,
|
val jwtSecret: String,
|
||||||
val postgresql: Postgresql,
|
val postgresql: Postgresql,
|
||||||
val rabbitmq: RabbitMQ,
|
val rabbitmq: RabbitMQ,
|
||||||
@@ -25,7 +24,6 @@ data class Configuration(
|
|||||||
val ApplicationConfig.configuration
|
val ApplicationConfig.configuration
|
||||||
get() =
|
get() =
|
||||||
Configuration(
|
Configuration(
|
||||||
redisUrl = getProperty("redis.url"),
|
|
||||||
jwtSecret = getProperty("jwt.secret"),
|
jwtSecret = getProperty("jwt.secret"),
|
||||||
postgresql =
|
postgresql =
|
||||||
Configuration.Postgresql(
|
Configuration.Postgresql(
|
||||||
-7
@@ -7,8 +7,6 @@ import org.koin.core.module.Module
|
|||||||
import org.koin.core.scope.Scope
|
import org.koin.core.scope.Scope
|
||||||
import org.koin.core.scope.ScopeCallback
|
import org.koin.core.scope.ScopeCallback
|
||||||
import org.koin.dsl.bind
|
import org.koin.dsl.bind
|
||||||
import redis.clients.jedis.JedisPooled
|
|
||||||
import redis.clients.jedis.UnifiedJedis
|
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
|
||||||
fun Module.configureDIDataSource(config: Configuration) {
|
fun Module.configureDIDataSource(config: Configuration) {
|
||||||
@@ -26,11 +24,6 @@ fun Module.configureDIDataSource(config: Configuration) {
|
|||||||
}
|
}
|
||||||
} bind DataSource::class
|
} bind DataSource::class
|
||||||
|
|
||||||
// Redis (for Projections)
|
|
||||||
single {
|
|
||||||
JedisPooled(config.redisUrl)
|
|
||||||
} bind UnifiedJedis::class
|
|
||||||
|
|
||||||
// RabbitMQ (for EventBus)
|
// RabbitMQ (for EventBus)
|
||||||
factory {
|
factory {
|
||||||
ConnectionFactory().apply {
|
ConnectionFactory().apply {
|
||||||
+1
-1
@@ -2,7 +2,7 @@ package eventDemo.contexts.auth.application.eventStores
|
|||||||
|
|
||||||
import eventDemo.contexts.auth.application.ports.UserEventStore
|
import eventDemo.contexts.auth.application.ports.UserEventStore
|
||||||
import eventDemo.contexts.auth.domain.User
|
import eventDemo.contexts.auth.domain.User
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.UserId
|
||||||
|
|
||||||
class UserEventStoreRepository(
|
class UserEventStoreRepository(
|
||||||
val eventStore: UserEventStore,
|
val eventStore: UserEventStore,
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.contexts.auth.application.eventStores
|
package eventDemo.contexts.auth.application.eventStores
|
||||||
|
|
||||||
import eventDemo.contexts.auth.domain.User
|
import eventDemo.contexts.auth.domain.User
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.UserId
|
||||||
|
|
||||||
interface UserRepository {
|
interface UserRepository {
|
||||||
fun get(id: UserId): User?
|
fun get(id: UserId): User?
|
||||||
+1
-1
@@ -2,6 +2,6 @@ package eventDemo.contexts.auth.application.ports
|
|||||||
|
|
||||||
import eventDemo.contexts.auth.domain.events.UserEvent
|
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||||
import eventDemo.libs.eventSource.eventStore.EventStore
|
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.UserId
|
||||||
|
|
||||||
interface UserEventStore : EventStore<UserEvent, UserId>
|
interface UserEventStore : EventStore<UserEvent, UserId>
|
||||||
+1
-1
@@ -2,7 +2,7 @@ package eventDemo.contexts.auth.domain
|
|||||||
|
|
||||||
import eventDemo.contexts.auth.domain.events.NewUserCreatedEvent
|
import eventDemo.contexts.auth.domain.events.NewUserCreatedEvent
|
||||||
import eventDemo.contexts.auth.domain.events.UserEvent
|
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.UserId
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.contexts.auth.domain.events
|
package eventDemo.contexts.auth.domain.events
|
||||||
|
|
||||||
import eventDemo.libs.eventSource.EventId
|
import eventDemo.shared.ids.EventId
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.UserId
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.contexts.auth.domain.events
|
package eventDemo.contexts.auth.domain.events
|
||||||
|
|
||||||
import eventDemo.libs.eventSource.Event
|
import eventDemo.libs.eventSource.Event
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.UserId
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import com.auth0.jwt.algorithms.Algorithm
|
|||||||
import eventDemo.configuration.configuration
|
import eventDemo.configuration.configuration
|
||||||
import eventDemo.contexts.auth.domain.User
|
import eventDemo.contexts.auth.domain.User
|
||||||
import eventDemo.contexts.auth.infrastructure.persistence.projection.UserProjection
|
import eventDemo.contexts.auth.infrastructure.persistence.projection.UserProjection
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.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
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import eventDemo.contexts.auth.application.ports.UserEventStore
|
|||||||
import eventDemo.contexts.auth.domain.events.UserEvent
|
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||||
import eventDemo.libs.eventSource.eventStore.EventStore
|
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||||
import eventDemo.libs.eventSource.eventStore.EventStoreInMemory
|
import eventDemo.libs.eventSource.eventStore.EventStoreInMemory
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.UserId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stream to publish and read the user events.
|
* A stream to publish and read the user events.
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import eventDemo.contexts.auth.application.ports.UserEventStore
|
|||||||
import eventDemo.contexts.auth.domain.events.UserEvent
|
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||||
import eventDemo.libs.eventSource.eventStore.EventStore
|
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||||
import eventDemo.libs.eventSource.eventStore.EventStoreInPostgresql
|
import eventDemo.libs.eventSource.eventStore.EventStoreInPostgresql
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.UserId
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package eventDemo.contexts.auth.infrastructure.persistence.projection
|
package eventDemo.contexts.auth.infrastructure.persistence.projection
|
||||||
|
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.UserId
|
||||||
|
|
||||||
data class UserProjection(
|
data class UserProjection(
|
||||||
val id: UserId,
|
val id: UserId,
|
||||||
+5
-4
@@ -3,9 +3,10 @@ package eventDemo.contexts.auth.infrastructure.persistence.projection
|
|||||||
import eventDemo.contexts.auth.application.ports.UserProjectionRepository
|
import eventDemo.contexts.auth.application.ports.UserProjectionRepository
|
||||||
import eventDemo.contexts.auth.infrastructure.checkPassword
|
import eventDemo.contexts.auth.infrastructure.checkPassword
|
||||||
import eventDemo.contexts.auth.infrastructure.hashPassword
|
import eventDemo.contexts.auth.infrastructure.hashPassword
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.UserId
|
||||||
import java.util.UUID
|
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
import kotlin.uuid.toJavaUuid
|
||||||
|
|
||||||
class UserProjectionRepositoryInPostgresql(
|
class UserProjectionRepositoryInPostgresql(
|
||||||
val dataSource: DataSource,
|
val dataSource: DataSource,
|
||||||
@@ -24,7 +25,7 @@ class UserProjectionRepositoryInPostgresql(
|
|||||||
}.use { resultSet ->
|
}.use { resultSet ->
|
||||||
if (resultSet.next()) {
|
if (resultSet.next()) {
|
||||||
UserProjection(
|
UserProjection(
|
||||||
id = UserId(UUID.fromString(resultSet.getString("id"))),
|
id = UserId(Uuid.parse(resultSet.getString("id"))),
|
||||||
username = resultSet.getString("username"),
|
username = resultSet.getString("username"),
|
||||||
password = resultSet.getString("password"),
|
password = resultSet.getString("password"),
|
||||||
)
|
)
|
||||||
@@ -42,7 +43,7 @@ class UserProjectionRepositoryInPostgresql(
|
|||||||
values (?, ?)
|
values (?, ?)
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
).use {
|
).use {
|
||||||
it.setObject(1, user.id)
|
it.setObject(1, user.id.id.toJavaUuid())
|
||||||
it.setString(2, user.username)
|
it.setString(2, user.username)
|
||||||
it.executeUpdate()
|
it.executeUpdate()
|
||||||
}
|
}
|
||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
package eventDemo.contexts.game.application.channels
|
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.CommandSubscriber
|
||||||
import eventDemo.contexts.game.application.notification.EventToNotificationSubscriber
|
import eventDemo.contexts.game.application.notification.EventToNotificationSubscriber
|
||||||
import eventDemo.contexts.game.application.notification.models.Notification
|
import eventDemo.shared.game.command.GameCommand
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.game.notification.Notification
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.ids.GameId
|
||||||
|
import eventDemo.shared.ids.UserId
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
import kotlinx.coroutines.channels.SendChannel
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
+6
-6
@@ -1,11 +1,11 @@
|
|||||||
package eventDemo.contexts.game.application.command.handlers
|
package eventDemo.contexts.game.application.command.handlers
|
||||||
|
|
||||||
import eventDemo.contexts.game.application.command.models.GameCommand
|
import eventDemo.shared.game.command.GameCommand
|
||||||
import eventDemo.contexts.game.application.command.models.JoinTheGameCommand
|
import eventDemo.shared.game.command.JoinTheGameCommand
|
||||||
import eventDemo.contexts.game.application.command.models.PlayCardCommand
|
import eventDemo.shared.game.command.PlayCardCommand
|
||||||
import eventDemo.contexts.game.application.command.models.ReadyToPlayCommand
|
import eventDemo.shared.game.command.ReadyToPlayCommand
|
||||||
import eventDemo.contexts.game.application.command.models.TakeCartFromDrawPileCommand
|
import eventDemo.shared.game.command.TakeCartFromDrawPileCommand
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.ids.GameId
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|
||||||
class GameCommandHandlerDispatcher(
|
class GameCommandHandlerDispatcher(
|
||||||
+4
-12
@@ -1,12 +1,12 @@
|
|||||||
package eventDemo.contexts.game.application.command.handlers
|
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.eventStores.GameRepository
|
||||||
import eventDemo.contexts.game.application.ports.GameEventBus
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
import eventDemo.contexts.game.domain.events.GameEvent
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
import eventDemo.contexts.game.domain.game.gameState.Game
|
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||||
import eventDemo.libs.command.Command
|
|
||||||
import eventDemo.libs.eventSource.eventStore.VersionConflictException
|
import eventDemo.libs.eventSource.eventStore.VersionConflictException
|
||||||
|
import eventDemo.shared.command.Command
|
||||||
|
import eventDemo.shared.game.command.GameCommand
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@@ -37,16 +37,8 @@ abstract class GameEventManager(
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun <G : Game> Game.isStatusOrFail(
|
protected inline fun <reified G : Game> Game.isStatusOrFail(message: String): G =
|
||||||
kClass: KClass<G>,
|
this as? G ?: throw CommandException(message)
|
||||||
message: String,
|
|
||||||
): G {
|
|
||||||
if (kClass.isInstance(this)) {
|
|
||||||
return this as G
|
|
||||||
} else {
|
|
||||||
throw CommandException(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun <T> retry(
|
protected fun <T> retry(
|
||||||
mapAttempts: Int = 5,
|
mapAttempts: Int = 5,
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
package eventDemo.contexts.game.application.command.handlers
|
package eventDemo.contexts.game.application.command.handlers
|
||||||
|
|
||||||
import eventDemo.contexts.auth.application.eventStores.UserRepository
|
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.eventStores.GameRepository
|
||||||
import eventDemo.contexts.game.application.ports.GameEventBus
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
||||||
|
import eventDemo.shared.game.command.JoinTheGameCommand
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command to perform an action to play a new card
|
* A command to perform an action to play a new card
|
||||||
@@ -20,7 +20,7 @@ class JoinTheGameHandler(
|
|||||||
retry {
|
retry {
|
||||||
command
|
command
|
||||||
.getGame()
|
.getGame()
|
||||||
.isStatusOrFail(GameCreated::class, "The game is started")
|
.isStatusOrFail<GameCreated>("The game is started")
|
||||||
.userJoinTheGame(
|
.userJoinTheGame(
|
||||||
userId = command.userId,
|
userId = command.userId,
|
||||||
name = user.username,
|
name = user.username,
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
package eventDemo.contexts.game.application.command.handlers
|
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.eventStores.GameRepository
|
||||||
import eventDemo.contexts.game.application.ports.GameEventBus
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
||||||
|
import eventDemo.shared.game.command.PlayCardCommand
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command to perform an action to play a new card
|
* A command to perform an action to play a new card
|
||||||
@@ -16,7 +16,7 @@ class PlayCardHandler(
|
|||||||
override fun handle(command: PlayCardCommand) {
|
override fun handle(command: PlayCardCommand) {
|
||||||
command
|
command
|
||||||
.getGame()
|
.getGame()
|
||||||
.isStatusOrFail(GameStarted::class, "The game is not started")
|
.isStatusOrFail<GameStarted>("The game is not started")
|
||||||
.playTheCard(
|
.playTheCard(
|
||||||
card = command.payload.card,
|
card = command.payload.card,
|
||||||
playerId = command.payload.playerId,
|
playerId = command.payload.playerId,
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
package eventDemo.contexts.game.application.command.handlers
|
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.eventStores.GameRepository
|
||||||
import eventDemo.contexts.game.application.ports.GameEventBus
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
||||||
|
import eventDemo.shared.game.command.ReadyToPlayCommand
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command to set as ready to play
|
* A command to set as ready to play
|
||||||
@@ -16,7 +16,7 @@ class ReadyToPlayHandler(
|
|||||||
override fun handle(command: ReadyToPlayCommand) {
|
override fun handle(command: ReadyToPlayCommand) {
|
||||||
command
|
command
|
||||||
.getGame()
|
.getGame()
|
||||||
.isStatusOrFail(GameCreated::class, "The game is started")
|
.isStatusOrFail<GameCreated>("The game is started")
|
||||||
.setReadyPlayer(command.payload.playerId)
|
.setReadyPlayer(command.payload.playerId)
|
||||||
.saveEvents()
|
.saveEvents()
|
||||||
.publishEvents()
|
.publishEvents()
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
package eventDemo.contexts.game.application.command.handlers
|
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.eventStores.GameRepository
|
||||||
import eventDemo.contexts.game.application.ports.GameEventBus
|
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||||
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
||||||
|
import eventDemo.shared.game.command.TakeCartFromDrawPileCommand
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command to draw card on draw pile.
|
* A command to draw card on draw pile.
|
||||||
@@ -18,7 +18,7 @@ class TakeCartFromDrawPileHandler(
|
|||||||
override fun handle(command: TakeCartFromDrawPileCommand) {
|
override fun handle(command: TakeCartFromDrawPileCommand) {
|
||||||
command
|
command
|
||||||
.getGame()
|
.getGame()
|
||||||
.isStatusOrFail(GameStarted::class, "The game is not started")
|
.isStatusOrFail<GameStarted>("The game is not started")
|
||||||
.playerTakeCartFromDrawPile(command.payload.playerId, 1)
|
.playerTakeCartFromDrawPile(command.payload.playerId, 1)
|
||||||
.saveEvents()
|
.saveEvents()
|
||||||
.publishEvents()
|
.publishEvents()
|
||||||
+1
-1
@@ -1,9 +1,9 @@
|
|||||||
package eventDemo.contexts.game.application.eventStores
|
package eventDemo.contexts.game.application.eventStores
|
||||||
|
|
||||||
import eventDemo.contexts.game.application.ports.GameEventStore
|
import eventDemo.contexts.game.application.ports.GameEventStore
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
|
||||||
import eventDemo.contexts.game.domain.game.gameState.Game
|
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||||
import eventDemo.libs.eventSource.eventStore.VersionConflictException
|
import eventDemo.libs.eventSource.eventStore.VersionConflictException
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
|
|
||||||
class GameEventStoreRepository(
|
class GameEventStoreRepository(
|
||||||
val eventStore: GameEventStore,
|
val eventStore: GameEventStore,
|
||||||
+1
-1
@@ -1,10 +1,10 @@
|
|||||||
package eventDemo.contexts.game.application.eventStores
|
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.Game
|
||||||
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
||||||
import eventDemo.contexts.game.domain.game.gameState.GameInit
|
import eventDemo.contexts.game.domain.game.gameState.GameInit
|
||||||
import eventDemo.libs.eventSource.eventStore.VersionConflictException
|
import eventDemo.libs.eventSource.eventStore.VersionConflictException
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
|
|
||||||
interface GameRepository {
|
interface GameRepository {
|
||||||
fun get(id: GameId): Game?
|
fun get(id: GameId): Game?
|
||||||
+25
-20
@@ -1,16 +1,5 @@
|
|||||||
package eventDemo.contexts.game.application.notification
|
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.CardIsPlayedEvent
|
||||||
import eventDemo.contexts.game.domain.events.DrawFilledWithDiscardEvent
|
import eventDemo.contexts.game.domain.events.DrawFilledWithDiscardEvent
|
||||||
import eventDemo.contexts.game.domain.events.GameCreatedEvent
|
import eventDemo.contexts.game.domain.events.GameCreatedEvent
|
||||||
@@ -23,7 +12,22 @@ import eventDemo.contexts.game.domain.events.PlayerReadyEvent
|
|||||||
import eventDemo.contexts.game.domain.events.PlayerWinEvent
|
import eventDemo.contexts.game.domain.events.PlayerWinEvent
|
||||||
import eventDemo.contexts.game.domain.game.gameState.Game
|
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||||
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.game.notification.ItsTheTurnOfNotification
|
||||||
|
import eventDemo.shared.game.notification.Notification
|
||||||
|
import eventDemo.shared.game.notification.PilesShuffledNotification
|
||||||
|
import eventDemo.shared.game.notification.PlayerAsJoinTheGameNotification
|
||||||
|
import eventDemo.shared.game.notification.PlayerAsPlayACardNotification
|
||||||
|
import eventDemo.shared.game.notification.PlayerHavePassNotification
|
||||||
|
import eventDemo.shared.game.notification.PlayerWasReadyNotification
|
||||||
|
import eventDemo.shared.game.notification.PlayerWinNotification
|
||||||
|
import eventDemo.shared.game.notification.TheGameWasStartedNotification
|
||||||
|
import eventDemo.shared.game.notification.WelcomeToTheGameNotification
|
||||||
|
import eventDemo.shared.game.notification.YourNewCardNotification
|
||||||
|
import eventDemo.shared.ids.UserId
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import io.github.oshai.kotlinlogging.withLoggingContext
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
fun GameEvent.toNotification(
|
fun GameEvent.toNotification(
|
||||||
game: Game,
|
game: Game,
|
||||||
@@ -31,15 +35,16 @@ fun GameEvent.toNotification(
|
|||||||
): Iterable<Notification> =
|
): Iterable<Notification> =
|
||||||
Iterable {
|
Iterable {
|
||||||
iterator {
|
iterator {
|
||||||
val currentPlayerId = game.players.get(currentUserId).id
|
|
||||||
|
|
||||||
context(iterator: SequenceScope<Notification>)
|
context(iterator: SequenceScope<Notification>)
|
||||||
suspend fun Notification.send() {
|
suspend fun Notification.send() {
|
||||||
|
withLoggingContext("notification" to (this).toString()) {
|
||||||
|
logger.info { "Notification sent" }
|
||||||
iterator.yield(this)
|
iterator.yield(this)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun PlayerActionEvent.isFromCurrentUser(): Boolean =
|
fun PlayerActionEvent.isFromCurrentUser(): Boolean =
|
||||||
currentPlayerId != playerId
|
game.players.get(currentUserId).id == playerId
|
||||||
|
|
||||||
when (this@toNotification) {
|
when (this@toNotification) {
|
||||||
is GameCreatedEvent -> {
|
is GameCreatedEvent -> {
|
||||||
@@ -51,7 +56,7 @@ fun GameEvent.toNotification(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is NewPlayerEvent -> {
|
is NewPlayerEvent -> {
|
||||||
if (this@toNotification.isFromCurrentUser()) {
|
if (this@toNotification.player.userId != currentUserId) {
|
||||||
PlayerAsJoinTheGameNotification(
|
PlayerAsJoinTheGameNotification(
|
||||||
player = this@toNotification.player,
|
player = this@toNotification.player,
|
||||||
).send()
|
).send()
|
||||||
@@ -85,6 +90,7 @@ fun GameEvent.toNotification(
|
|||||||
|
|
||||||
if (game is GameStarted) {
|
if (game is GameStarted) {
|
||||||
ItsTheTurnOfNotification(player = game.nextPlayer)
|
ItsTheTurnOfNotification(player = game.nextPlayer)
|
||||||
|
.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,21 +107,20 @@ fun GameEvent.toNotification(
|
|||||||
|
|
||||||
if (game is GameStarted) {
|
if (game is GameStarted) {
|
||||||
ItsTheTurnOfNotification(player = game.nextPlayer)
|
ItsTheTurnOfNotification(player = game.nextPlayer)
|
||||||
|
.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is PlayerReadyEvent -> {
|
is PlayerReadyEvent -> {
|
||||||
if (this@toNotification.isFromCurrentUser()) {
|
|
||||||
PlayerWasReadyNotification(
|
PlayerWasReadyNotification(
|
||||||
playerId = this@toNotification.playerId,
|
playerId = this@toNotification.playerId,
|
||||||
)
|
).send()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is PlayerWinEvent -> {
|
is PlayerWinEvent -> {
|
||||||
PlayerWinNotification(
|
PlayerWinNotification(
|
||||||
playerId = this@toNotification.playerId,
|
playerId = this@toNotification.playerId,
|
||||||
)
|
).send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-4
@@ -1,7 +1,6 @@
|
|||||||
package eventDemo.contexts.game.application.notification
|
package eventDemo.contexts.game.application.notification
|
||||||
|
|
||||||
import eventDemo.contexts.game.application.command.handlers.GameCommandHandlerDispatcher
|
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.eventStores.GameRepository
|
||||||
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Command
|
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Command
|
||||||
import eventDemo.contexts.game.application.logging.LoggingContextKeys.CurrentUserId
|
import eventDemo.contexts.game.application.logging.LoggingContextKeys.CurrentUserId
|
||||||
@@ -9,12 +8,13 @@ import eventDemo.contexts.game.application.logging.LoggingContextKeys.Event
|
|||||||
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Game
|
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Game
|
||||||
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Notification
|
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Notification
|
||||||
import eventDemo.contexts.game.application.logging.withLoggingContext
|
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.application.ports.GameEventBus
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
|
||||||
import eventDemo.libs.bus.Bus
|
import eventDemo.libs.bus.Bus
|
||||||
import eventDemo.libs.command.CommandUnicityChecker
|
import eventDemo.libs.command.CommandUnicityChecker
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.game.command.GameCommand
|
||||||
|
import eventDemo.shared.game.notification.Notification
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
|
import eventDemo.shared.ids.UserId
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.contexts.game.application.ports
|
package eventDemo.contexts.game.application.ports
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.events.GameEvent
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
|
||||||
import eventDemo.libs.eventSource.eventStore.EventStore
|
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
|
|
||||||
interface GameEventStore : EventStore<GameEvent, GameId>
|
interface GameEventStore : EventStore<GameEvent, GameId>
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package eventDemo.domain.event.projection
|
package eventDemo.domain.event.projection
|
||||||
|
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameList
|
import eventDemo.shared.game.projection.GameList
|
||||||
|
|
||||||
interface GameListRepository {
|
interface GameListRepository {
|
||||||
fun getList(
|
fun getList(
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package eventDemo.contexts.game.application.ports
|
package eventDemo.contexts.game.application.ports
|
||||||
|
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameProjection
|
|
||||||
import eventDemo.libs.bus.Bus
|
import eventDemo.libs.bus.Bus
|
||||||
|
import eventDemo.shared.game.projection.GameProjection
|
||||||
|
|
||||||
interface GameProjectionBus : Bus<GameProjection>
|
interface GameProjectionBus : Bus<GameProjection>
|
||||||
+1
-1
@@ -9,7 +9,7 @@ import eventDemo.contexts.game.domain.events.NewPlayerEvent
|
|||||||
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
|
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
|
||||||
import eventDemo.contexts.game.domain.events.PlayerReadyEvent
|
import eventDemo.contexts.game.domain.events.PlayerReadyEvent
|
||||||
import eventDemo.contexts.game.domain.events.PlayerWinEvent
|
import eventDemo.contexts.game.domain.events.PlayerWinEvent
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameList
|
import eventDemo.shared.game.projection.GameList
|
||||||
|
|
||||||
fun GameList.applyEvent(event: GameEvent): GameList =
|
fun GameList.applyEvent(event: GameEvent): GameList =
|
||||||
when (event) {
|
when (event) {
|
||||||
+1
-1
@@ -48,7 +48,7 @@ class ReactionListener(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun sendWinnerEvent(game: Game) {
|
private fun sendWinnerEvent(game: Game) {
|
||||||
if (game is GameStarted) {
|
if (game is GameStarted && game.lastPlayerId != null) {
|
||||||
val lastPlayerWin =
|
val lastPlayerWin =
|
||||||
game
|
game
|
||||||
.players
|
.players
|
||||||
+8
-8
@@ -1,15 +1,15 @@
|
|||||||
package eventDemo.contexts.game.domain.events
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.game.Card
|
import eventDemo.shared.game.Card
|
||||||
import eventDemo.contexts.game.domain.game.Card.Color
|
import eventDemo.shared.game.Card.Color
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.game.Player
|
||||||
import eventDemo.contexts.game.domain.game.Player
|
import eventDemo.shared.ids.EventId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
import eventDemo.shared.ids.GameId
|
||||||
import eventDemo.libs.eventSource.EventId
|
import eventDemo.shared.serializers.EventIdSerializer
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [GameEvent] to represent a played card.
|
* An [GameEvent] to represent a played card.
|
||||||
@@ -24,7 +24,7 @@ data class CardIsPlayedEvent(
|
|||||||
) : GameEvent,
|
) : GameEvent,
|
||||||
PlayerActionEvent {
|
PlayerActionEvent {
|
||||||
@Serializable(with = EventIdSerializer::class)
|
@Serializable(with = EventIdSerializer::class)
|
||||||
override val eventId: EventId = EventId(UUID.randomUUID())
|
override val eventId: EventId = EventId(Uuid.random())
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
|
|
||||||
val theColorCard get() = if (card is Card.CardWithColor) card.color else chosenColor
|
val theColorCard get() = if (card is Card.CardWithColor) card.color else chosenColor
|
||||||
+7
-7
@@ -1,14 +1,14 @@
|
|||||||
package eventDemo.contexts.game.domain.events
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.game.DiscardPile
|
import eventDemo.shared.game.DiscardPile
|
||||||
import eventDemo.contexts.game.domain.game.DrawPile
|
import eventDemo.shared.game.DrawPile
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.ids.EventId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
import eventDemo.shared.ids.GameId
|
||||||
import eventDemo.libs.eventSource.EventId
|
import eventDemo.shared.serializers.EventIdSerializer
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the Pile are shuffled after the draw pille was empty
|
* When the Pile are shuffled after the draw pille was empty
|
||||||
@@ -21,6 +21,6 @@ class DrawFilledWithDiscardEvent(
|
|||||||
override val version: Int,
|
override val version: Int,
|
||||||
) : GameEvent {
|
) : GameEvent {
|
||||||
@Serializable(with = EventIdSerializer::class)
|
@Serializable(with = EventIdSerializer::class)
|
||||||
override val eventId: EventId = EventId(UUID.randomUUID())
|
override val eventId: EventId = EventId(Uuid.random())
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
+5
-5
@@ -1,12 +1,12 @@
|
|||||||
package eventDemo.contexts.game.domain.events
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.ids.EventId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
import eventDemo.shared.ids.GameId
|
||||||
import eventDemo.libs.eventSource.EventId
|
import eventDemo.shared.serializers.EventIdSerializer
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This [GameEvent] is sent when all players are ready.
|
* This [GameEvent] is sent when all players are ready.
|
||||||
@@ -17,6 +17,6 @@ data class GameCreatedEvent(
|
|||||||
override val version: Int,
|
override val version: Int,
|
||||||
) : GameEvent {
|
) : GameEvent {
|
||||||
@Serializable(with = EventIdSerializer::class)
|
@Serializable(with = EventIdSerializer::class)
|
||||||
override val eventId: EventId = EventId(UUID.randomUUID())
|
override val eventId: EventId = EventId(Uuid.random())
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
+4
-4
@@ -1,10 +1,10 @@
|
|||||||
package eventDemo.contexts.game.domain.events
|
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.Event
|
||||||
import eventDemo.libs.eventSource.EventId
|
import eventDemo.shared.ids.EventId
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
|
import eventDemo.shared.serializers.EventIdSerializer
|
||||||
|
import eventDemo.shared.serializers.GameIdSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
|
import eventDemo.shared.game.DiscardPile
|
||||||
|
import eventDemo.shared.game.DrawPile
|
||||||
|
import eventDemo.shared.game.Player
|
||||||
|
import eventDemo.shared.game.PlayerHand
|
||||||
|
import eventDemo.shared.ids.EventId
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
|
import eventDemo.shared.serializers.EventIdSerializer
|
||||||
|
import eventDemo.shared.serializers.PlayerIdSerializer
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.uuid.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.random())
|
||||||
|
override val createdAt: Instant = Clock.System.now()
|
||||||
|
}
|
||||||
+6
-6
@@ -1,13 +1,13 @@
|
|||||||
package eventDemo.contexts.game.domain.events
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.game.Player
|
||||||
import eventDemo.contexts.game.domain.game.Player
|
import eventDemo.shared.ids.EventId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
import eventDemo.shared.ids.GameId
|
||||||
import eventDemo.libs.eventSource.EventId
|
import eventDemo.shared.serializers.EventIdSerializer
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [GameEvent] to represent a new player joining the game.
|
* An [GameEvent] to represent a new player joining the game.
|
||||||
@@ -22,6 +22,6 @@ data class NewPlayerEvent(
|
|||||||
override val playerId: Player.PlayerId get() = player.id
|
override val playerId: Player.PlayerId get() = player.id
|
||||||
|
|
||||||
@Serializable(with = EventIdSerializer::class)
|
@Serializable(with = EventIdSerializer::class)
|
||||||
override val eventId: EventId = EventId(UUID.randomUUID())
|
override val eventId: EventId = EventId(Uuid.random())
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package eventDemo.contexts.game.domain.events
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.game.Player
|
import eventDemo.shared.game.Player
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
+8
-8
@@ -1,15 +1,15 @@
|
|||||||
package eventDemo.contexts.game.domain.events
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.game.Card
|
import eventDemo.shared.game.Card
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.game.Player
|
||||||
import eventDemo.contexts.game.domain.game.Player
|
import eventDemo.shared.ids.EventId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
import eventDemo.shared.ids.GameId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
|
import eventDemo.shared.serializers.EventIdSerializer
|
||||||
import eventDemo.libs.eventSource.EventId
|
import eventDemo.shared.serializers.PlayerIdSerializer
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This [GameEvent] is sent when a player can play.
|
* This [GameEvent] is sent when a player can play.
|
||||||
@@ -24,6 +24,6 @@ data class PlayerHaveDrawCardEvent(
|
|||||||
) : GameEvent,
|
) : GameEvent,
|
||||||
PlayerActionEvent {
|
PlayerActionEvent {
|
||||||
@Serializable(with = EventIdSerializer::class)
|
@Serializable(with = EventIdSerializer::class)
|
||||||
override val eventId: EventId = EventId(UUID.randomUUID())
|
override val eventId: EventId = EventId(Uuid.random())
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
+7
-7
@@ -1,14 +1,14 @@
|
|||||||
package eventDemo.contexts.game.domain.events
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.game.Player
|
||||||
import eventDemo.contexts.game.domain.game.Player
|
import eventDemo.shared.ids.EventId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
import eventDemo.shared.ids.GameId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
|
import eventDemo.shared.serializers.EventIdSerializer
|
||||||
import eventDemo.libs.eventSource.EventId
|
import eventDemo.shared.serializers.PlayerIdSerializer
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This [GameEvent] is sent when a player is ready.
|
* This [GameEvent] is sent when a player is ready.
|
||||||
@@ -22,6 +22,6 @@ data class PlayerReadyEvent(
|
|||||||
) : GameEvent,
|
) : GameEvent,
|
||||||
PlayerActionEvent {
|
PlayerActionEvent {
|
||||||
@Serializable(with = EventIdSerializer::class)
|
@Serializable(with = EventIdSerializer::class)
|
||||||
override val eventId: EventId = EventId(UUID.randomUUID())
|
override val eventId: EventId = EventId(Uuid.random())
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
+7
-7
@@ -1,14 +1,14 @@
|
|||||||
package eventDemo.contexts.game.domain.events
|
package eventDemo.contexts.game.domain.events
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.game.Player
|
||||||
import eventDemo.contexts.game.domain.game.Player
|
import eventDemo.shared.ids.EventId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
import eventDemo.shared.ids.GameId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
|
import eventDemo.shared.serializers.EventIdSerializer
|
||||||
import eventDemo.libs.eventSource.EventId
|
import eventDemo.shared.serializers.PlayerIdSerializer
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This [GameEvent] is sent when a player is ready.
|
* This [GameEvent] is sent when a player is ready.
|
||||||
@@ -22,6 +22,6 @@ data class PlayerWinEvent(
|
|||||||
) : GameEvent,
|
) : GameEvent,
|
||||||
PlayerActionEvent {
|
PlayerActionEvent {
|
||||||
@Serializable(with = EventIdSerializer::class)
|
@Serializable(with = EventIdSerializer::class)
|
||||||
override val eventId: EventId = EventId(UUID.randomUUID())
|
override val eventId: EventId = EventId(Uuid.random())
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
+3
-3
@@ -1,11 +1,11 @@
|
|||||||
package eventDemo.contexts.game.domain.game.errors
|
package eventDemo.contexts.game.domain.game.errors
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.events.GameEvent
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
import eventDemo.contexts.game.domain.game.Card
|
|
||||||
import eventDemo.contexts.game.domain.game.Player
|
|
||||||
import eventDemo.contexts.game.domain.game.PlayerList
|
|
||||||
import eventDemo.contexts.game.domain.game.gameState.Deck
|
import eventDemo.contexts.game.domain.game.gameState.Deck
|
||||||
import eventDemo.contexts.game.domain.game.gameState.Game
|
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||||
|
import eventDemo.shared.game.Card
|
||||||
|
import eventDemo.shared.game.Player
|
||||||
|
import eventDemo.shared.game.PlayerList
|
||||||
|
|
||||||
abstract class GameException(
|
abstract class GameException(
|
||||||
message: String,
|
message: String,
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package eventDemo.contexts.game.domain.game.gameState
|
||||||
|
|
||||||
|
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.domain.game.errors.GameException
|
||||||
|
import eventDemo.contexts.game.domain.game.errors.InconsistentEventVersionException
|
||||||
|
import eventDemo.shared.game.PlayerList
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
|
|
||||||
|
sealed interface Game {
|
||||||
|
val aggregateId: GameId
|
||||||
|
val players: PlayerList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On each modification, an event is put in their
|
||||||
|
*/
|
||||||
|
val recordedEvents: Set<GameEvent>
|
||||||
|
val version: Int
|
||||||
|
|
||||||
|
enum class Direction {
|
||||||
|
CLOCKWISE,
|
||||||
|
COUNTER_CLOCKWISE,
|
||||||
|
;
|
||||||
|
|
||||||
|
fun revert(): Direction =
|
||||||
|
if (this === CLOCKWISE) {
|
||||||
|
COUNTER_CLOCKWISE
|
||||||
|
} else {
|
||||||
|
CLOCKWISE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun loadFromHistory(events: Set<GameEvent>): Game =
|
||||||
|
events
|
||||||
|
.fold(GameInit(events.first().aggregateId)) { game: Game, event ->
|
||||||
|
game.run {
|
||||||
|
when (event) {
|
||||||
|
is GameCreatedEvent if this is GameInit -> applyEvent(event)
|
||||||
|
is GameCreatedEvent -> error("Game is already created")
|
||||||
|
is NewPlayerEvent if this is GameCreated -> applyEvent(event)
|
||||||
|
is NewPlayerEvent -> error("Game is already stared")
|
||||||
|
is PlayerReadyEvent if this is GameCreated -> applyEvent(event)
|
||||||
|
is PlayerReadyEvent -> error("Game is already stared")
|
||||||
|
is GameStartedEvent if this is GameCreated -> applyEvent(event)
|
||||||
|
is GameStartedEvent -> error("Game is already started")
|
||||||
|
is CardIsPlayedEvent if this is GameStarted -> applyEvent(event)
|
||||||
|
is CardIsPlayedEvent -> error("Game is end")
|
||||||
|
is PlayerHaveDrawCardEvent if this is GameStarted -> applyEvent(event)
|
||||||
|
is PlayerHaveDrawCardEvent -> error("Game is end")
|
||||||
|
is PlayerWinEvent if this is GameStarted -> applyEvent(event)
|
||||||
|
is PlayerWinEvent -> error("Game is end")
|
||||||
|
is DrawFilledWithDiscardEvent if this is GameStarted -> applyEvent(event)
|
||||||
|
is DrawFilledWithDiscardEvent -> error("Game is end")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.let {
|
||||||
|
when (it) {
|
||||||
|
is GameInit -> it
|
||||||
|
is GameCreated -> it.copy(recordedEvents = emptySet())
|
||||||
|
is GameEnded -> it.copy(recordedEvents = emptySet())
|
||||||
|
is GameStarted -> it.copy(recordedEvents = emptySet())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun <T : GameEvent> T.checkState(
|
||||||
|
block: (T) -> Boolean,
|
||||||
|
exception: (T) -> GameException,
|
||||||
|
): T {
|
||||||
|
if (!block(this)) throw exception(this)
|
||||||
|
return this
|
||||||
|
}
|
||||||
+59
-19
@@ -4,17 +4,18 @@ import eventDemo.contexts.game.domain.events.GameEvent
|
|||||||
import eventDemo.contexts.game.domain.events.GameStartedEvent
|
import eventDemo.contexts.game.domain.events.GameStartedEvent
|
||||||
import eventDemo.contexts.game.domain.events.NewPlayerEvent
|
import eventDemo.contexts.game.domain.events.NewPlayerEvent
|
||||||
import eventDemo.contexts.game.domain.events.PlayerReadyEvent
|
import eventDemo.contexts.game.domain.events.PlayerReadyEvent
|
||||||
import eventDemo.contexts.game.domain.game.Card
|
|
||||||
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.PlayerList
|
|
||||||
import eventDemo.contexts.game.domain.game.errors.AllPlayerNotReadyException
|
import eventDemo.contexts.game.domain.game.errors.AllPlayerNotReadyException
|
||||||
import eventDemo.contexts.game.domain.game.errors.DeckMissingCardsException
|
import eventDemo.contexts.game.domain.game.errors.DeckMissingCardsException
|
||||||
import eventDemo.contexts.game.domain.game.errors.NeedMorePlayersToStartGameException
|
import eventDemo.contexts.game.domain.game.errors.NeedMorePlayersToStartGameException
|
||||||
import eventDemo.contexts.game.domain.game.errors.ThePlayerIsNotInTheGameException
|
import eventDemo.contexts.game.domain.game.errors.ThePlayerIsNotInTheGameException
|
||||||
import eventDemo.sharedKernel.UserId
|
import eventDemo.shared.game.Card
|
||||||
|
import eventDemo.shared.game.DiscardPile
|
||||||
|
import eventDemo.shared.game.DrawPile
|
||||||
|
import eventDemo.shared.game.Player
|
||||||
|
import eventDemo.shared.game.PlayerHand
|
||||||
|
import eventDemo.shared.game.PlayerList
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
|
import eventDemo.shared.ids.UserId
|
||||||
|
|
||||||
data class GameCreated(
|
data class GameCreated(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
@@ -23,24 +24,28 @@ data class GameCreated(
|
|||||||
override val recordedEvents: Set<GameEvent>,
|
override val recordedEvents: Set<GameEvent>,
|
||||||
override val version: Int,
|
override val version: Int,
|
||||||
) : Game {
|
) : Game {
|
||||||
init {
|
|
||||||
checkRecorderEventsConsistency()
|
|
||||||
}
|
|
||||||
|
|
||||||
val allPlayerIsReady: Boolean
|
val allPlayerIsReady: Boolean
|
||||||
get() {
|
get() {
|
||||||
return playersStatus.values.all { it == PlayerStatus.Ready }
|
return playersStatus.isNotEmpty() && playersStatus.values.all { it == PlayerStatus.Ready }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startGame(deck: Deck = newDeck().shuffleDeck()): GameStarted {
|
fun startGame(deck: Deck = newDeck().shuffleDeck()): GameStarted {
|
||||||
val (drawPile, discardPile) = initPiles(deck)
|
val (drawPile, discardPile, playersHands) =
|
||||||
|
initPiles(deck)
|
||||||
|
.let { (drawPile, discardPile) ->
|
||||||
|
createHandsFromDrawPile(drawPile)
|
||||||
|
.let { (drawPile, playersHands) ->
|
||||||
|
Triple(drawPile, discardPile, playersHands)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return GameStartedEvent(
|
return GameStartedEvent(
|
||||||
aggregateId = aggregateId,
|
aggregateId = aggregateId,
|
||||||
firstPlayer = players.random().id,
|
firstPlayer = players.randomPlayer().id,
|
||||||
version = version + 1,
|
version = version + 1,
|
||||||
drawPile = drawPile,
|
drawPile = drawPile,
|
||||||
discardPile = discardPile,
|
discardPile = discardPile,
|
||||||
|
playersHans = playersHands,
|
||||||
).checkState(
|
).checkState(
|
||||||
{ players.size > 1 },
|
{ players.size > 1 },
|
||||||
{ NeedMorePlayersToStartGameException(players) },
|
{ NeedMorePlayersToStartGameException(players) },
|
||||||
@@ -50,7 +55,8 @@ data class GameCreated(
|
|||||||
).checkState(
|
).checkState(
|
||||||
{ deck.size == 108 },
|
{ deck.size == 108 },
|
||||||
{ DeckMissingCardsException(players, deck) },
|
{ DeckMissingCardsException(players, deck) },
|
||||||
).run(::applyEvent)
|
).also { if (it.drawPile.size + it.discardPile.size + playersHands.values.sumOf { it.size } != 108) error("missing cards!") }
|
||||||
|
.run(::applyEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initPiles(deck: Set<Card>): Pair<DrawPile, DiscardPile> =
|
private fun initPiles(deck: Set<Card>): Pair<DrawPile, DiscardPile> =
|
||||||
@@ -61,6 +67,20 @@ data class GameCreated(
|
|||||||
draw to DiscardPile(cards)
|
draw to DiscardPile(cards)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createHandsFromDrawPile(drawPile: DrawPile) =
|
||||||
|
players
|
||||||
|
.map { it.id }
|
||||||
|
.fold(Pair(drawPile, emptyMap<Player.PlayerId, PlayerHand>())) { (drawAcc, handsAcc), playerId ->
|
||||||
|
drawAcc
|
||||||
|
.take(7)
|
||||||
|
.let { (draw, hand) ->
|
||||||
|
Pair(
|
||||||
|
draw,
|
||||||
|
handsAcc + (playerId to PlayerHand(hand)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun userJoinTheGame(
|
fun userJoinTheGame(
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -107,8 +127,13 @@ data class GameCreated(
|
|||||||
internal fun applyEvent(event: GameStartedEvent): GameStarted =
|
internal fun applyEvent(event: GameStartedEvent): GameStarted =
|
||||||
GameStarted(
|
GameStarted(
|
||||||
aggregateId = event.aggregateId,
|
aggregateId = event.aggregateId,
|
||||||
players = players,
|
players =
|
||||||
lastPlayerId = event.firstPlayer,
|
players
|
||||||
|
.map {
|
||||||
|
it.copy(hand = event.playersHans[it.id] ?: error("Player ${it.id} not found"))
|
||||||
|
}.let { PlayerList(it.toSet()) },
|
||||||
|
lastPlayerId = null,
|
||||||
|
nextPlayerId = event.firstPlayer,
|
||||||
drawPile = event.drawPile,
|
drawPile = event.drawPile,
|
||||||
discardPile = event.discardPile,
|
discardPile = event.discardPile,
|
||||||
version = event.version + 1,
|
version = event.version + 1,
|
||||||
@@ -133,7 +158,22 @@ fun newDeck(): Deck =
|
|||||||
(1..2).map { Card.PassCard(color) }
|
(1..2).map { Card.PassCard(color) }
|
||||||
}.let {
|
}.let {
|
||||||
it + (1..4).map { Card.Plus4Card() }
|
it + (1..4).map { Card.Plus4Card() }
|
||||||
|
}.let {
|
||||||
|
it + (1..4).map { Card.ChangeColorCard() }
|
||||||
}.toSet()
|
}.toSet()
|
||||||
|
|
||||||
fun Set<Card>.shuffleDeck() =
|
fun Set<Card>.shuffleDeck(): Set<Card> {
|
||||||
shuffled().toSet()
|
if (isDisabled) return this
|
||||||
|
return shuffled().toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PlayerList.randomPlayer(): Player {
|
||||||
|
if (isDisabled) return first()
|
||||||
|
return random()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isDisabled = false
|
||||||
|
|
||||||
|
fun disableRandomForTest() {
|
||||||
|
isDisabled = true
|
||||||
|
}
|
||||||
+3
-4
@@ -1,9 +1,9 @@
|
|||||||
package eventDemo.contexts.game.domain.game.gameState
|
package eventDemo.contexts.game.domain.game.gameState
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.events.GameEvent
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.game.Player
|
||||||
import eventDemo.contexts.game.domain.game.Player
|
import eventDemo.shared.game.PlayerList
|
||||||
import eventDemo.contexts.game.domain.game.PlayerList
|
import eventDemo.shared.ids.GameId
|
||||||
|
|
||||||
data class GameEnded(
|
data class GameEnded(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
@@ -16,6 +16,5 @@ data class GameEnded(
|
|||||||
if (!players.map { it.id }.containsAll(playerWins)) {
|
if (!players.map { it.id }.containsAll(playerWins)) {
|
||||||
throw IllegalArgumentException("Player ${players.map { it.id }} were not in players")
|
throw IllegalArgumentException("Player ${players.map { it.id }} were not in players")
|
||||||
}
|
}
|
||||||
checkRecorderEventsConsistency()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -2,8 +2,8 @@ package eventDemo.contexts.game.domain.game.gameState
|
|||||||
|
|
||||||
import eventDemo.contexts.game.domain.events.GameCreatedEvent
|
import eventDemo.contexts.game.domain.events.GameCreatedEvent
|
||||||
import eventDemo.contexts.game.domain.events.GameEvent
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.game.PlayerList
|
||||||
import eventDemo.contexts.game.domain.game.PlayerList
|
import eventDemo.shared.ids.GameId
|
||||||
|
|
||||||
data class GameInit(
|
data class GameInit(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
+53
-45
@@ -6,13 +6,6 @@ import eventDemo.contexts.game.domain.events.GameEvent
|
|||||||
import eventDemo.contexts.game.domain.events.PlayerActionEvent
|
import eventDemo.contexts.game.domain.events.PlayerActionEvent
|
||||||
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
|
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
|
||||||
import eventDemo.contexts.game.domain.events.PlayerWinEvent
|
import eventDemo.contexts.game.domain.events.PlayerWinEvent
|
||||||
import eventDemo.contexts.game.domain.game.Card
|
|
||||||
import eventDemo.contexts.game.domain.game.Card.Color
|
|
||||||
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.PlayerList
|
|
||||||
import eventDemo.contexts.game.domain.game.errors.InconsistentGameException
|
import eventDemo.contexts.game.domain.game.errors.InconsistentGameException
|
||||||
import eventDemo.contexts.game.domain.game.errors.ItsNotTheTurnException
|
import eventDemo.contexts.game.domain.game.errors.ItsNotTheTurnException
|
||||||
import eventDemo.contexts.game.domain.game.errors.TheCardHasNoColorException
|
import eventDemo.contexts.game.domain.game.errors.TheCardHasNoColorException
|
||||||
@@ -22,42 +15,22 @@ import eventDemo.contexts.game.domain.game.errors.ThePlayerHasRemainingCardsExce
|
|||||||
import eventDemo.contexts.game.domain.game.errors.ThePlayerIsNotInTheGameException
|
import eventDemo.contexts.game.domain.game.errors.ThePlayerIsNotInTheGameException
|
||||||
import eventDemo.contexts.game.domain.game.errors.ThePlayerMustPlayACardException
|
import eventDemo.contexts.game.domain.game.errors.ThePlayerMustPlayACardException
|
||||||
import eventDemo.contexts.game.domain.game.gameState.Game.Direction
|
import eventDemo.contexts.game.domain.game.gameState.Game.Direction
|
||||||
|
import eventDemo.shared.game.Card
|
||||||
|
import eventDemo.shared.game.Card.Color
|
||||||
|
import eventDemo.shared.game.DiscardPile
|
||||||
|
import eventDemo.shared.game.DrawPile
|
||||||
|
import eventDemo.shared.game.Player
|
||||||
|
import eventDemo.shared.game.PlayerList
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
|
|
||||||
data class GameStarted(
|
fun PlayerList.nextPlayerTurn(
|
||||||
override val aggregateId: GameId,
|
lastPlayerId: Player.PlayerId,
|
||||||
override val players: PlayerList,
|
direction: Direction,
|
||||||
val drawPile: DrawPile,
|
): Player.PlayerId {
|
||||||
val discardPile: DiscardPile,
|
val lastPlayer = get(lastPlayerId)
|
||||||
val lastPlayerId: Player.PlayerId,
|
val playersLastTurn = filter { it.hand.cards.isNotEmpty() || it == lastPlayer }
|
||||||
val currentColor: Color,
|
|
||||||
val playedTurnHistory: List<History> = emptyList(),
|
|
||||||
val direction: Direction = Direction.CLOCKWISE,
|
|
||||||
val playerWins: Set<Player.PlayerId> = emptySet(),
|
|
||||||
override val version: Int,
|
|
||||||
override val recordedEvents: Set<GameEvent>,
|
|
||||||
) : Game {
|
|
||||||
val playersInGame by lazy { players.filter { it.hand.cards.isNotEmpty() } }
|
|
||||||
|
|
||||||
val lastPlayedCard: Card? by lazy { discardPile.topCard }
|
return playersLastTurn
|
||||||
|
|
||||||
val lastPlayed: Player by lazy { players.get(lastPlayerId) }
|
|
||||||
|
|
||||||
init {
|
|
||||||
checkRecorderEventsConsistency()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class History(
|
|
||||||
val playerId: Player.PlayerId,
|
|
||||||
val event: GameEvent,
|
|
||||||
val direction: Direction,
|
|
||||||
)
|
|
||||||
|
|
||||||
val lastPlayer by lazy { players.get(lastPlayerId) }
|
|
||||||
|
|
||||||
val nextPlayer: Player by lazy {
|
|
||||||
val playersLastTurn = players.filter { it.hand.cards.isNotEmpty() || it == lastPlayer }
|
|
||||||
|
|
||||||
playersLastTurn
|
|
||||||
.indexOf(lastPlayer)
|
.indexOf(lastPlayer)
|
||||||
.let { lastPlayerIndex ->
|
.let { lastPlayerIndex ->
|
||||||
if (direction == Direction.CLOCKWISE) {
|
if (direction == Direction.CLOCKWISE) {
|
||||||
@@ -73,8 +46,36 @@ data class GameStarted(
|
|||||||
lastPlayerIndex - 1
|
lastPlayerIndex - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.let { nextPlayerIndex -> players.elementAt(nextPlayerIndex) }
|
}.let { nextPlayerIndex -> elementAt(nextPlayerIndex).id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GameStarted(
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
override val players: PlayerList,
|
||||||
|
val drawPile: DrawPile,
|
||||||
|
val discardPile: DiscardPile,
|
||||||
|
val lastPlayerId: Player.PlayerId?,
|
||||||
|
val nextPlayerId: Player.PlayerId,
|
||||||
|
val currentColor: Color,
|
||||||
|
val playedTurnHistory: List<History> = emptyList(),
|
||||||
|
val direction: Direction = Direction.CLOCKWISE,
|
||||||
|
val playerWins: Set<Player.PlayerId> = emptySet(),
|
||||||
|
override val version: Int,
|
||||||
|
override val recordedEvents: Set<GameEvent>,
|
||||||
|
) : Game {
|
||||||
|
val playersInGame by lazy { players.filter { it.hand.cards.isNotEmpty() } }
|
||||||
|
|
||||||
|
val lastPlayedCard: Card? by lazy { discardPile.topCard }
|
||||||
|
|
||||||
|
data class History(
|
||||||
|
val playerId: Player.PlayerId,
|
||||||
|
val event: GameEvent,
|
||||||
|
val direction: Direction,
|
||||||
|
)
|
||||||
|
|
||||||
|
val lastPlayer: Player? by lazy { lastPlayerId?.let { players.get(it) } }
|
||||||
|
|
||||||
|
val nextPlayer: Player by lazy { players.get(nextPlayerId) }
|
||||||
|
|
||||||
fun canBePlayThisCard(card: Card): Boolean {
|
fun canBePlayThisCard(card: Card): Boolean {
|
||||||
val cardOnBoard = discardPile.topCard ?: return false
|
val cardOnBoard = discardPile.topCard ?: return false
|
||||||
@@ -144,8 +145,12 @@ data class GameStarted(
|
|||||||
): GameStarted =
|
): GameStarted =
|
||||||
CardIsPlayedEvent(aggregateId, card, playerId, chosenColor, version + 1)
|
CardIsPlayedEvent(aggregateId, card, playerId, chosenColor, version + 1)
|
||||||
.checkPlayerTurn()
|
.checkPlayerTurn()
|
||||||
.checkState({ card is Card.CardWithColor && chosenColor != null }, { TheCardIsAColorCardException(playerId) })
|
.checkState({
|
||||||
.checkState({ card is Card.CardWith4Color && chosenColor == null }, { TheCardHasNoColorException(playerId) })
|
(card is Card.CardWithColor && chosenColor == null) || card is Card.CardWith4Color
|
||||||
|
}, { TheCardIsAColorCardException(playerId) })
|
||||||
|
.checkState({
|
||||||
|
(card is Card.CardWith4Color && chosenColor != null) || card is Card.CardWithColor
|
||||||
|
}, { TheCardHasNoColorException(playerId) })
|
||||||
.run(::applyEvent)
|
.run(::applyEvent)
|
||||||
|
|
||||||
internal fun applyEvent(event: CardIsPlayedEvent): GameStarted =
|
internal fun applyEvent(event: CardIsPlayedEvent): GameStarted =
|
||||||
@@ -167,6 +172,7 @@ data class GameStarted(
|
|||||||
discardPile = discardPile.withNewCard(card = event.card),
|
discardPile = discardPile.withNewCard(card = event.card),
|
||||||
currentColor = color,
|
currentColor = color,
|
||||||
lastPlayerId = event.playerId,
|
lastPlayerId = event.playerId,
|
||||||
|
nextPlayerId = players.nextPlayerTurn(event.playerId, nextDirectionAfterPlay),
|
||||||
playedTurnHistory = playedTurnHistory - History(event.playerId, event, direction),
|
playedTurnHistory = playedTurnHistory - History(event.playerId, event, direction),
|
||||||
direction = nextDirectionAfterPlay,
|
direction = nextDirectionAfterPlay,
|
||||||
version = event.version,
|
version = event.version,
|
||||||
@@ -202,6 +208,8 @@ data class GameStarted(
|
|||||||
copy(
|
copy(
|
||||||
players = players.withNewCardOnPlayerHand(event.playerId, event.takenCards),
|
players = players.withNewCardOnPlayerHand(event.playerId, event.takenCards),
|
||||||
drawPile = drawPile.take(event.takenCards.size).first,
|
drawPile = drawPile.take(event.takenCards.size).first,
|
||||||
|
lastPlayerId = event.playerId,
|
||||||
|
nextPlayerId = players.nextPlayerTurn(event.playerId, direction),
|
||||||
version = event.version,
|
version = event.version,
|
||||||
recordedEvents = recordedEvents + event,
|
recordedEvents = recordedEvents + event,
|
||||||
)
|
)
|
||||||
+1
-15
@@ -1,5 +1,6 @@
|
|||||||
package eventDemo.contexts.game.infrastructure.configuration.ktor
|
package eventDemo.contexts.game.infrastructure.configuration.ktor
|
||||||
|
|
||||||
|
import eventDemo.shared.http.HttpErrorBadRequest
|
||||||
import io.ktor.http.HttpHeaders
|
import io.ktor.http.HttpHeaders
|
||||||
import io.ktor.http.HttpMethod
|
import io.ktor.http.HttpMethod
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
@@ -10,7 +11,6 @@ import io.ktor.server.plugins.cors.routing.CORS
|
|||||||
import io.ktor.server.plugins.statuspages.StatusPages
|
import io.ktor.server.plugins.statuspages.StatusPages
|
||||||
import io.ktor.server.resources.Resources
|
import io.ktor.server.resources.Resources
|
||||||
import io.ktor.server.response.respondText
|
import io.ktor.server.response.respondText
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
fun Application.configureHttpRouting() {
|
fun Application.configureHttpRouting() {
|
||||||
install(CORS) {
|
install(CORS) {
|
||||||
@@ -38,17 +38,3 @@ fun Application.configureHttpRouting() {
|
|||||||
class BadRequestException(
|
class BadRequestException(
|
||||||
val httpError: HttpErrorBadRequest,
|
val httpError: HttpErrorBadRequest,
|
||||||
) : Exception()
|
) : Exception()
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class HttpErrorBadRequest(
|
|
||||||
val title: String = HttpStatusCode.BadRequest.description,
|
|
||||||
val invalidParams: List<InvalidParam> = emptyList(),
|
|
||||||
) {
|
|
||||||
val statusCode: Int = HttpStatusCode.BadRequest.value
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class InvalidParam(
|
|
||||||
val name: String,
|
|
||||||
val reason: String,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+11
-11
@@ -1,21 +1,21 @@
|
|||||||
package eventDemo.contexts.game.infrastructure.configuration.ktor
|
package eventDemo.contexts.game.infrastructure.configuration.ktor
|
||||||
|
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.game.Player
|
||||||
import eventDemo.contexts.game.domain.game.Player
|
import eventDemo.shared.ids.CommandId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.CommandIdSerializer
|
import eventDemo.shared.ids.EventId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
|
import eventDemo.shared.ids.GameId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
|
import eventDemo.shared.serializers.CommandIdSerializer
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
|
import eventDemo.shared.serializers.EventIdSerializer
|
||||||
import eventDemo.libs.command.CommandId
|
import eventDemo.shared.serializers.GameIdSerializer
|
||||||
import eventDemo.libs.eventSource.EventId
|
import eventDemo.shared.serializers.PlayerIdSerializer
|
||||||
import eventDemo.libs.serializer.UUIDSerializer
|
import eventDemo.shared.serializers.UUIDSerializer
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import java.util.UUID
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
fun Application.configureSerialization() {
|
fun Application.configureSerialization() {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
@@ -29,7 +29,7 @@ fun defaultJsonSerializer(): Json =
|
|||||||
Json {
|
Json {
|
||||||
serializersModule =
|
serializersModule =
|
||||||
SerializersModule {
|
SerializersModule {
|
||||||
contextual(UUID::class) { UUIDSerializer }
|
contextual(Uuid::class) { UUIDSerializer }
|
||||||
contextual(GameId::class) { GameIdSerializer }
|
contextual(GameId::class) { GameIdSerializer }
|
||||||
contextual(EventId::class) { EventIdSerializer }
|
contextual(EventId::class) { EventIdSerializer }
|
||||||
contextual(CommandId::class) { CommandIdSerializer }
|
contextual(CommandId::class) { CommandIdSerializer }
|
||||||
+1
-1
@@ -2,9 +2,9 @@ package eventDemo.contexts.game.infrastructure.persistence.eventStore
|
|||||||
|
|
||||||
import eventDemo.contexts.game.application.ports.GameEventStore
|
import eventDemo.contexts.game.application.ports.GameEventStore
|
||||||
import eventDemo.contexts.game.domain.events.GameEvent
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
|
||||||
import eventDemo.libs.eventSource.eventStore.EventStore
|
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||||
import eventDemo.libs.eventSource.eventStore.EventStoreInMemory
|
import eventDemo.libs.eventSource.eventStore.EventStoreInMemory
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stream to publish and read the played card event.
|
* A stream to publish and read the played card event.
|
||||||
+1
-1
@@ -2,9 +2,9 @@ package eventDemo.contexts.game.infrastructure.persistence.eventStore
|
|||||||
|
|
||||||
import eventDemo.contexts.game.application.ports.GameEventStore
|
import eventDemo.contexts.game.application.ports.GameEventStore
|
||||||
import eventDemo.contexts.game.domain.events.GameEvent
|
import eventDemo.contexts.game.domain.events.GameEvent
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
|
||||||
import eventDemo.libs.eventSource.eventStore.EventStore
|
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||||
import eventDemo.libs.eventSource.eventStore.EventStoreInPostgresql
|
import eventDemo.libs.eventSource.eventStore.EventStoreInPostgresql
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
|
||||||
+2
-2
@@ -4,9 +4,9 @@ import eventDemo.contexts.game.application.ports.GameEventBus
|
|||||||
import eventDemo.contexts.game.application.ports.GameEventStore
|
import eventDemo.contexts.game.application.ports.GameEventStore
|
||||||
import eventDemo.contexts.game.application.ports.GameProjectionBus
|
import eventDemo.contexts.game.application.ports.GameProjectionBus
|
||||||
import eventDemo.contexts.game.application.projections.applyEvent
|
import eventDemo.contexts.game.application.projections.applyEvent
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameList
|
|
||||||
import eventDemo.domain.event.projection.GameListRepository
|
import eventDemo.domain.event.projection.GameListRepository
|
||||||
|
import eventDemo.shared.game.projection.GameList
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
import io.github.oshai.kotlinlogging.withLoggingContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
+1
-1
@@ -1,9 +1,9 @@
|
|||||||
package eventDemo.contexts.game.infrastructure.persistence.projections.bus
|
package eventDemo.contexts.game.infrastructure.persistence.projections.bus
|
||||||
|
|
||||||
import eventDemo.contexts.game.application.ports.GameProjectionBus
|
import eventDemo.contexts.game.application.ports.GameProjectionBus
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameProjection
|
|
||||||
import eventDemo.libs.bus.Bus
|
import eventDemo.libs.bus.Bus
|
||||||
import eventDemo.libs.bus.BusInMemory
|
import eventDemo.libs.bus.BusInMemory
|
||||||
|
import eventDemo.shared.game.projection.GameProjection
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class GameProjectionBusInMemory :
|
class GameProjectionBusInMemory :
|
||||||
+1
-1
@@ -2,9 +2,9 @@ package eventDemo.contexts.game.infrastructure.persistence.projections.bus
|
|||||||
|
|
||||||
import com.rabbitmq.client.ConnectionFactory
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
import eventDemo.contexts.game.application.ports.GameProjectionBus
|
import eventDemo.contexts.game.application.ports.GameProjectionBus
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameProjection
|
|
||||||
import eventDemo.libs.bus.Bus
|
import eventDemo.libs.bus.Bus
|
||||||
import eventDemo.libs.bus.BusInRabbitMQ
|
import eventDemo.libs.bus.BusInRabbitMQ
|
||||||
|
import eventDemo.shared.game.projection.GameProjection
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
+2
-2
@@ -3,8 +3,8 @@ package eventDemo.contexts.game.infrastructure.rest
|
|||||||
import eventDemo.contexts.game.application.eventStores.GameRepository
|
import eventDemo.contexts.game.application.eventStores.GameRepository
|
||||||
import eventDemo.contexts.game.application.notification.toNotification
|
import eventDemo.contexts.game.application.notification.toNotification
|
||||||
import eventDemo.contexts.game.application.ports.GameEventStore
|
import eventDemo.contexts.game.application.ports.GameEventStore
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
import eventDemo.shared.ids.GameId
|
||||||
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
|
import eventDemo.shared.serializers.GameIdSerializer
|
||||||
import eventDemo.sharedKernel.currentUserId
|
import eventDemo.sharedKernel.currentUserId
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.resources.Resource
|
import io.ktor.resources.Resource
|
||||||
+3
-3
@@ -1,22 +1,22 @@
|
|||||||
package eventDemo.contexts.game.infrastructure.websocket
|
package eventDemo.contexts.game.infrastructure.websocket
|
||||||
|
|
||||||
import eventDemo.contexts.game.application.channels.GameChannelsSubscriber
|
import eventDemo.contexts.game.application.channels.GameChannelsSubscriber
|
||||||
import eventDemo.contexts.game.domain.game.GameId
|
|
||||||
import eventDemo.libs.helpers.fromFrameChannel
|
import eventDemo.libs.helpers.fromFrameChannel
|
||||||
import eventDemo.libs.helpers.toObjectChannel
|
import eventDemo.libs.helpers.toObjectChannel
|
||||||
|
import eventDemo.shared.ids.GameId
|
||||||
import eventDemo.sharedKernel.currentUserId
|
import eventDemo.sharedKernel.currentUserId
|
||||||
import io.ktor.server.auth.authenticate
|
import io.ktor.server.auth.authenticate
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.server.routing.Route
|
||||||
import io.ktor.server.websocket.webSocket
|
import io.ktor.server.websocket.webSocket
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import java.util.UUID
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
@DelicateCoroutinesApi
|
||||||
fun Route.gameWebSocket(channelSubscriber: GameChannelsSubscriber) {
|
fun Route.gameWebSocket(channelSubscriber: GameChannelsSubscriber) {
|
||||||
authenticate {
|
authenticate {
|
||||||
webSocket("/games/{id}") {
|
webSocket("/games/{id}") {
|
||||||
channelSubscriber.subscribePlayerToGameChannels(
|
channelSubscriber.subscribePlayerToGameChannels(
|
||||||
gameId = GameId(UUID.nameUUIDFromBytes(call.parameters["id"]?.encodeToByteArray()!!)),
|
gameId = GameId(Uuid.parse(call.parameters["id"]!!)),
|
||||||
userId = call.currentUserId,
|
userId = call.currentUserId,
|
||||||
incomingCommandChannel = toObjectChannel(incoming),
|
incomingCommandChannel = toObjectChannel(incoming),
|
||||||
sendNotificationChannel = fromFrameChannel(outgoing),
|
sendNotificationChannel = fromFrameChannel(outgoing),
|
||||||
+5
-1
@@ -78,7 +78,11 @@ class BusInRabbitMQ<E>(
|
|||||||
body: ByteArray,
|
body: ByteArray,
|
||||||
) {
|
) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
block(stringToObject(body.toString(Charsets.UTF_8)))
|
val obj = stringToObject(body.toString(Charsets.UTF_8))
|
||||||
|
withLoggingContext("item" to obj.toString()) {
|
||||||
|
logger.info { "Received delivery of $exchangeName" }
|
||||||
|
}
|
||||||
|
block(obj)
|
||||||
}
|
}
|
||||||
channel.basicAck(envelope.deliveryTag, false)
|
channel.basicAck(envelope.deliveryTag, false)
|
||||||
}
|
}
|
||||||
+2
@@ -1,5 +1,7 @@
|
|||||||
package eventDemo.libs.command
|
package eventDemo.libs.command
|
||||||
|
|
||||||
|
import eventDemo.shared.command.Command
|
||||||
|
import eventDemo.shared.ids.CommandId
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package eventDemo.libs.eventSource
|
||||||
|
|
||||||
|
import eventDemo.shared.ids.AggregateId
|
||||||
|
import eventDemo.shared.ids.EventId
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The basic interface for an Event
|
||||||
|
* @see eventDemo.libs.eventSource.eventStore.EventStream
|
||||||
|
*/
|
||||||
|
interface Event<ID : AggregateId> {
|
||||||
|
val eventId: EventId
|
||||||
|
val aggregateId: ID
|
||||||
|
val createdAt: Instant
|
||||||
|
val version: Int
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.libs.eventSource.eventStore
|
package eventDemo.libs.eventSource.eventStore
|
||||||
|
|
||||||
import eventDemo.libs.eventSource.AggregateId
|
|
||||||
import eventDemo.libs.eventSource.Event
|
import eventDemo.libs.eventSource.Event
|
||||||
|
import eventDemo.shared.ids.AggregateId
|
||||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
import io.github.oshai.kotlinlogging.withLoggingContext
|
||||||
|
|
||||||
interface EventStore<E : Event<ID>, ID : AggregateId> {
|
interface EventStore<E : Event<ID>, ID : AggregateId> {
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.libs.eventSource.eventStore
|
package eventDemo.libs.eventSource.eventStore
|
||||||
|
|
||||||
import eventDemo.libs.eventSource.AggregateId
|
|
||||||
import eventDemo.libs.eventSource.Event
|
import eventDemo.libs.eventSource.Event
|
||||||
|
import eventDemo.shared.ids.AggregateId
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.ConcurrentMap
|
import java.util.concurrent.ConcurrentMap
|
||||||
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.libs.eventSource.eventStore
|
package eventDemo.libs.eventSource.eventStore
|
||||||
|
|
||||||
import eventDemo.libs.eventSource.AggregateId
|
|
||||||
import eventDemo.libs.eventSource.Event
|
import eventDemo.libs.eventSource.Event
|
||||||
|
import eventDemo.shared.ids.AggregateId
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
|
||||||
class EventStoreInPostgresql<E : Event<ID>, ID : AggregateId>(
|
class EventStoreInPostgresql<E : Event<ID>, ID : AggregateId>(
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.libs.eventSource.eventStore
|
package eventDemo.libs.eventSource.eventStore
|
||||||
|
|
||||||
import eventDemo.libs.eventSource.AggregateId
|
|
||||||
import eventDemo.libs.eventSource.Event
|
import eventDemo.libs.eventSource.Event
|
||||||
|
import eventDemo.shared.ids.AggregateId
|
||||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
import io.github.oshai.kotlinlogging.withLoggingContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.libs.eventSource.eventStore
|
package eventDemo.libs.eventSource.eventStore
|
||||||
|
|
||||||
import eventDemo.libs.eventSource.AggregateId
|
|
||||||
import eventDemo.libs.eventSource.Event
|
import eventDemo.libs.eventSource.Event
|
||||||
|
import eventDemo.shared.ids.AggregateId
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
+7
-6
@@ -1,12 +1,13 @@
|
|||||||
package eventDemo.libs.eventSource.eventStore
|
package eventDemo.libs.eventSource.eventStore
|
||||||
|
|
||||||
import eventDemo.libs.eventSource.AggregateId
|
|
||||||
import eventDemo.libs.eventSource.Event
|
import eventDemo.libs.eventSource.Event
|
||||||
|
import eventDemo.shared.ids.AggregateId
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
import io.github.oshai.kotlinlogging.withLoggingContext
|
||||||
import org.postgresql.util.PGobject
|
import org.postgresql.util.PGobject
|
||||||
import org.postgresql.util.PSQLException
|
import org.postgresql.util.PSQLException
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
import kotlin.uuid.toJavaUuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An In-Memory implementation of an event stream.
|
* An In-Memory implementation of an event stream.
|
||||||
@@ -39,8 +40,8 @@ class EventStreamInPostgresql<E : Event<ID>, ID : AggregateId>(
|
|||||||
on conflict (id) do nothing
|
on conflict (id) do nothing
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
).use {
|
).use {
|
||||||
it.setObject(1, event.eventId.id)
|
it.setObject(1, event.eventId.id.toJavaUuid())
|
||||||
it.setObject(2, event.aggregateId.id)
|
it.setObject(2, event.aggregateId.id.toJavaUuid())
|
||||||
it.setInt(3, event.version)
|
it.setInt(3, event.version)
|
||||||
it.setObject(4, PGJsonb(objectToString(event)))
|
it.setObject(4, PGJsonb(objectToString(event)))
|
||||||
it.executeUpdate()
|
it.executeUpdate()
|
||||||
@@ -69,7 +70,7 @@ class EventStreamInPostgresql<E : Event<ID>, ID : AggregateId>(
|
|||||||
order by version asc
|
order by version asc
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
).use {
|
).use {
|
||||||
it.setObject(1, aggregateId.id)
|
it.setObject(1, aggregateId.id.toJavaUuid())
|
||||||
it.executeQuery().use { resultSet ->
|
it.executeQuery().use { resultSet ->
|
||||||
buildSet {
|
buildSet {
|
||||||
while (resultSet.next()) {
|
while (resultSet.next()) {
|
||||||
@@ -95,7 +96,7 @@ class EventStreamInPostgresql<E : Event<ID>, ID : AggregateId>(
|
|||||||
order by version asc
|
order by version asc
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
).use {
|
).use {
|
||||||
it.setObject(1, aggregateId.id)
|
it.setObject(1, aggregateId.id.toJavaUuid())
|
||||||
it.executeQuery().use { resultSet ->
|
it.executeQuery().use { resultSet ->
|
||||||
resultSet.next()
|
resultSet.next()
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ class EventStreamInPostgresql<E : Event<ID>, ID : AggregateId>(
|
|||||||
).use { stmt ->
|
).use { stmt ->
|
||||||
stmt.setInt(1, version.first)
|
stmt.setInt(1, version.first)
|
||||||
stmt.setInt(2, version.last)
|
stmt.setInt(2, version.last)
|
||||||
stmt.setObject(3, aggregateId.id)
|
stmt.setObject(3, aggregateId.id.toJavaUuid())
|
||||||
stmt.executeQuery().use { resultSet ->
|
stmt.executeQuery().use { resultSet ->
|
||||||
buildSet {
|
buildSet {
|
||||||
while (resultSet.next()) {
|
while (resultSet.next()) {
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user