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
|
No files matched your search
@@ -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")
|
||||||
|
}
|
||||||
File renamed without changes.
-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(
|
||||||
File renamed without changes.
-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 {
|
||||||
File renamed without changes.
File renamed without changes.
+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>
|
||||||
File renamed without changes.
+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
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+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()
|
||||||
}
|
}
|
||||||
File renamed without changes.
File renamed without changes.
+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
|
||||||
File renamed without changes.
+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?
|
||||||
File renamed without changes.
File renamed without changes.
+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
|
||||||
File renamed without changes.
+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,
|
||||||
+52
-44
@@ -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,9 +46,37 @@ 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
|
||||||
return when (cardOnBoard) {
|
return when (cardOnBoard) {
|
||||||
@@ -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,
|
||||||
)
|
)
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+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 }
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+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
|
||||||
|
|
||||||
File renamed without changes.
+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),
|
||||||
File renamed without changes.
File renamed without changes.
+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()) {
|
||||||
Loaded 100 of 228 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user