sourcerer 0.1.0

Event-sourcing framework providing aggregates, repositories, stores, snapshots and upcasters for Rust applications.
Documentation

Event Sourcing Framework

sourcerer is a Rust framework for building event-sourced applications. It provides the core traits and components to get started with event sourcing, including aggregates, events, event stores, and repositories.

Core Concepts

  • [Aggregate]: A consistency boundary that processes commands and produces events.
  • [Event]: An immutable fact that represents a change in the state of an aggregate.
  • [EventStore]: A persistent store for events.
  • [SnapshotStore]: A persistent store for aggregate snapshots, used to optimize loading.
  • [Repository]: A high-level API for loading aggregates, handling commands, and saving events.

Example

// 1. Define your aggregate, events, and commands.
use sourcerer::{Aggregate, AggregateId, Event, Snapshot};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum BankAccountEvent {
    Opened { initial_balance: u64 },
    Credited { amount: u64 },
    Debited { amount: u64 },
}
impl Event for BankAccountEvent {
   fn event_type(&self) -> &'static str {
       match self {
           BankAccountEvent::Opened { .. } => "Opened",
           BankAccountEvent::Credited { .. } => "Credited",
           BankAccountEvent::Debited { .. } => "Debited",
       }
   }
   fn event_version(&self) -> u16 {
       1
   }
   fn event_source(&self) -> &'static str { "urn:sourcerer:bank" }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BankAccountSnapshot {
    balance: u64,
}
impl Snapshot for BankAccountSnapshot {}

#[derive(Debug)]
pub enum BankAccountCommand {
    Open { initial_balance: u64 },
    Deposit { amount: u64 },
    Withdraw { amount: u64 },
}

#[derive(Debug, Default)]
pub struct BankAccount {
    id: Uuid,
    balance: u64,
    version: i64,
}

// 2. Implement the Aggregate trait.
#[sourcerer::async_trait]
impl Aggregate for BankAccount {
    type Id = Uuid;
    type Event = BankAccountEvent;
    type Command = BankAccountCommand;
    type Snapshot = BankAccountSnapshot;
    type Error = std::convert::Infallible;

    fn id(&self) -> &Self::Id {
        &self.id
    }

    fn version(&self) -> i64 {
        self.version
    }

    fn apply(&mut self, event: &Self::Event) {
        match event {
            BankAccountEvent::Opened { initial_balance } => {
                self.balance = *initial_balance;
                self.id = Uuid::new_v4();
            }
            BankAccountEvent::Credited { amount } => {
                self.balance += *amount;
            }
            BankAccountEvent::Debited { amount } => {
                self.balance -= *amount;
            }
        }
        self.version += 1;
    }

    async fn handle(&self, command: Self::Command) -> Result<Vec<Self::Event>, Self::Error> {
        // Business logic and validation here...
        Ok(vec![])
    }

    fn from_snapshot(snapshot: Self::Snapshot) -> Self {
        Self {
            balance: snapshot.balance,
            ..Default::default()
        }
    }

    fn snapshot(&self) -> Self::Snapshot {
        BankAccountSnapshot {
            balance: self.balance
        }
    }
}

// 3. Use the repository to interact with your aggregate.
use sourcerer::store::in_memory::InMemoryEventStore;
use sourcerer::store::in_memory_snapshot::InMemorySnapshotStore;
use sourcerer::repository::GenericRepository;
use std::sync::Arc;

async fn bank_account_example() {
    let event_store = Arc::new(InMemoryEventStore::<BankAccount>::default());
    let snapshot_store = Arc::new(InMemorySnapshotStore::<BankAccount>::default());
    let repo = GenericRepository::new(event_store, Some(snapshot_store));

    // ...
}