Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New v0.6.0 #1

Merged
merged 10 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Rust CI/release

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

env:
CARGO_TERM_COLOR: always

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Clippy
run: cargo clippy
- name: Run tests
run: cargo run --example game

release:
if: ${{ github.event.pull_request.merged }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Clippy
run: cargo clippy
- name: Run tests
run: cargo run --example game
- uses: manoadamro/rust-release@v1
with:
owner: ${{ github.repository_owner }}
repo: ${{ github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
13 changes: 2 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,18 @@ name = "mini_cqrs"
description = "Minimal event sourcing components"
authors = ["Andrea Pavoni <[email protected]>"]
repository = "https://github.com/andreapavoni/pcqrs"
version = "0.5.0"
version = "0.6.0"
license = "MIT"
keywords = ["cqrs", "event-sourcing"]
rust-version = "1.67"
rust-version = "1.71"
edition = "2021"
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[[example]]
name = "simple"
path = "examples/simple.rs"

[[example]]
name = "game"
path = "examples/game.rs"


[[example]]
name = "game_snapshots"
path = "examples/game_snapshots.rs"

[dependencies]
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
Expand Down
55 changes: 35 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,44 +37,59 @@ mini_cqrs = { git = "https://github.com/andreapavoni/mini_cqrs" }

## Usage


Once you have added the library to your project's dependencies, you can start using it by importing the library:

```
use mini_cqrs::*;
```

Being made almost entirely of traits, Mini CQRS is very flexible but, of course,
it also requires a bit of boilerplate. For this reason, you can check out the
[examples directory](https://github.com/andreapavoni/mini_cqrs/tree/master/examples).
it also requires some boilerplate. For this reason, you can check out the
[examples directory](https://github.com/andreapavoni/mini_cqrs/tree/master/examples) for more details.

When you have implemented the various traits from the library, you can finally build your CQRS architecture.

Here's a snippet extracted from the [game example](https://github.com/andreapavoni/mini_cqrs/tree/master/examples/game.rs):

```rust

// Setup components
let store = InMemoryEventStore::new();
let repo = Arc::new(Mutex::new(InMemoryRepository::new()));
let snapshot_store = InMemorySnapshotStore::<GameState>::new();
let consumers = vec![GameEventConsumers::Counter(CounterConsumer::new(
repo.clone(),
))];
let aggregate_manager = SnapshotAggregateManager::new(snapshot_store);

let consumers = vec![GameEventConsumers::Counter(CounterConsumer::new(repo.clone()))];
let dispatcher: GameDispatcher = SimpleDispatcher::new(store, consumers);
let queries = AppQueries {};
let mut cqrs = Cqrs::new(dispatcher, queries);
// Setup a Cqrs instance
let mut cqrs = Cqrs::new(aggregate_manager, store, consumers);

// Init a command with some arguments
let player_1 = Player { id: "player_1".to_string(), points: 0, };
let player_2 = Player { id: "player_2".to_string(), points: 0, };

let main_id = "0123456789abcdef".to_string();
let command = GameCommand::StartGame { player_1: player_1.clone(), player_2: player_2.clone(), goal: 3, },

let id = cqrs.execute( main_id.clone(), command.clone()).await?;

assert_eq!(id, main_id.clone());

let query = GetGameQuery::new(main_id.clone(), repo.clone());
let result = cqrs.queries().run(query.clone()).await?.unwrap();
let main_id = Uuid::new_v4();
let start_cmd = CmdStartGame {
player_1: player_1.clone(),
player_2: player_2.clone(),
goal: 3,
};

// Execute the command
cqrs.execute(main_id, &start_cmd).await?;

// Query the read model
let q = GetGameQuery::new(main_id, repo.clone());
let result = cqrs.query(&q).await?.unwrap();
```

Spoiler: the entire code for the above example is ~500 lines of code.

## Documentation

Code is decently documented and is being improved. For real working examples you can check the
Code is documented and is being improved. For real working examples you can check the
[examples](https://github.com/andreapavoni/mini_cqrs/tree/master/examples).

There's also a small work-in-progress PoC/experiment called [simple_counter](https://github.com/andreapavoni/simple_counter) which I'm using as a playground for a more complex real world use case in terms of code and libraries integration.


## Contributing

Expand Down
92 changes: 34 additions & 58 deletions examples/game.rs
Original file line number Diff line number Diff line change
@@ -1,48 +1,34 @@
/// # Mini CQRS Example: Simple Game
///
/// This example demonstrates a more complex use case by introducing a read model with a repository
/// that can read and write to an in-memory database through an event consumer. While the
/// implementation is basic, it serves as a foundation for exploring and testing more realistic use
/// cases.
///
/// ## Overview
///
/// In this example, we simulate a simple game with the following components:
///
/// - **Game**: Represents the game state, including two players, a score goal, and game actions.
/// - **Players**: Two players compete to reach the score goal.
/// - **Actions**: Players can attack each other, earning points with each successful attack.
/// - **Win Condition**: The first player to reach the score goal wins the game.
///
/// # Mini CQRS Example: Game with SnapshotAggregateManager
///
/// ## Usage
///
/// ```sh
/// cargo run --example game
/// ```
/// You won't see nothing in the output, but that's because the code uses `assert`s to check that
/// it behaves as expected. The only output, if any, would be errors.
///

use std::sync::Arc;

use mini_cqrs::{Cqrs, QueriesRunner, SnapshotAggregateManager};
use tokio::sync::Mutex;

use mini_cqrs::{Cqrs, SimpleDispatcher, QueriesRunner};

#[path = "lib/common_game.rs"]
mod common_game;
use common_game::*;
use uuid::Uuid;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = InMemoryEventStore::new();
let repo = Arc::new(Mutex::new(InMemoryRepository::new()));
let snapshot_store = InMemorySnapshotStore::<GameState>::new();

let consumers = vec![GameEventConsumers::Counter(CounterConsumer::new(
repo.clone(),
))];

let consumers = vec![GameEventConsumers::Counter(CounterConsumer::new(repo.clone()))];
let dispatcher: GameDispatcher = SimpleDispatcher::new(store, consumers);
let queries = AppQueries {};
let mut cqrs = Cqrs::new(dispatcher, queries);
let aggregate_manager = SnapshotAggregateManager::new(snapshot_store);

let mut cqrs = Cqrs::new(aggregate_manager, store, consumers);

let player_1 = Player {
id: "player_1".to_string(),
Expand All @@ -53,60 +39,50 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
points: 0,
};

let main_id = "1".to_string();
let main_id = Uuid::new_v4();

cqrs.execute(
main_id.clone(),
GameCommand::StartGame {
player_1: player_1.clone(),
player_2: player_2.clone(),
goal: 3,
},
)
.await?;
let start_cmd = CmdStartGame {
player_1: player_1.clone(),
player_2: player_2.clone(),
goal: 3,
};

let q = GetGameQuery::new(main_id.clone(), repo.clone());
let result = cqrs.queries().run(q.clone()).await?.unwrap();
cqrs.execute(main_id, &start_cmd).await?;
let q = GetGameQuery::new(main_id, repo.clone());
let result = cqrs.query(&q).await?.unwrap();

assert_eq!(result.player_1.id, "player_1".to_string());
assert_eq!(result.player_2.id, "player_2".to_string());
assert_eq!(result.player_1.id, player_1.id);
assert_eq!(result.player_2.id, player_2.id);
verify_game_result(&result, 0, 0, 3, GameStatus::Playing);

let command = GameCommand::AttackPlayer {
let attack_cmd = CmdAttackPlayer {
attacker: player_1.clone(),
};
cqrs.execute(main_id, &attack_cmd).await?;
let result = cqrs.query(&q).await?.unwrap();

cqrs.execute(main_id.clone(), command.clone()).await?;

let result = cqrs.queries().run(q.clone()).await?.unwrap();
verify_game_result(&result, 1, 0, 3, GameStatus::Playing);

cqrs.execute(main_id.clone(), command.clone()).await?;
cqrs.execute(main_id, &attack_cmd).await?;
let result = cqrs.query(&q).await?.unwrap();

let result = cqrs.queries().run(q.clone()).await?.unwrap();
verify_game_result(&result, 2, 0, 3, GameStatus::Playing);

cqrs.execute(
main_id.clone(),
GameCommand::AttackPlayer {
attacker: player_2.clone(),
},
)
.await?;
let attack_cmd_2 = CmdAttackPlayer {
attacker: player_2.clone(),
};
cqrs.execute(main_id, &attack_cmd_2).await?;
let result = cqrs.query(&q).await?.unwrap();

let result = cqrs.queries().run(q.clone()).await?.unwrap();
verify_game_result(&result, 2, 1, 3, GameStatus::Playing);

cqrs.execute(main_id.clone(), command.clone()).await?;

cqrs.execute(main_id, &attack_cmd).await?;
let winner = Player {
id: player_1.id.clone(),
points: 3,
};
let result = cqrs.query(&q).await?.unwrap();

let result = cqrs.queries().run(q.clone()).await?.unwrap();
verify_game_result(&result, 3, 1, 3, GameStatus::Winner(winner));

Ok(())
}

Loading