use std::fmt;
use serde::{Deserialize, Serialize};
use ddd_cqrs_es::{Aggregate, DomainEvent};
/// Type-safe newtype wrapper for the Counter Aggregate ID.
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CounterId(pub String);
impl fmt::Display for CounterId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for CounterId {
fn from(id: String) -> Self {
Self(id)
}
}
impl From<&str> for CounterId {
fn from(id: &str) -> Self {
Self(id.to_string())
}
}
/// Commands accepted by the Counter Aggregate.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CounterCommand {
Increment { amount: i32 },
Decrement { amount: i32 },
Reset,
}
/// Domain events emitted by the Counter Aggregate.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CounterEvent {
Incremented { amount: i32 },
Decremented { amount: i32 },
ResetPerformed { value: i32 },
}
impl DomainEvent for CounterEvent {
fn event_type(&self) -> &'static str {
match self {
CounterEvent::Incremented { .. } => "incremented",
CounterEvent::Decremented { .. } => "decremented",
CounterEvent::ResetPerformed { .. } => "reset_performed",
}
}
}
/// The Counter Aggregate state.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Counter {
pub id: CounterId,
pub value: i32,
pub revision: u64,
}
impl Aggregate for Counter {
type Id = CounterId;
type Command = CounterCommand;
type Event = CounterEvent;
type Error = String;
fn aggregate_type() -> &'static str {
"counter"
}
fn revision(&self) -> u64 {
self.revision
}
fn new() -> Self {
Self {
id: CounterId(String::new()),
value: 0,
revision: 0,
}
}
/// Infallibly mutates state based on a committed event fact.
fn apply(&mut self, event: &Self::Event) {
match event {
CounterEvent::Incremented { amount } => {
self.value = self.value.saturating_add(*amount);
}
CounterEvent::Decremented { amount } => {
self.value = self.value.saturating_sub(*amount);
}
CounterEvent::ResetPerformed { value } => {
self.value = *value;
}
}
self.revision += 1;
}
/// Validates a command against current state. Returns a vector of events if successful.
fn handle(&self, command: Self::Command) -> Result<Vec<Self::Event>, Self::Error> {
match command {
CounterCommand::Increment { amount } => {
if amount <= 0 {
return Err("amount to increment must be positive".to_string());
}
if self.value.checked_add(amount).is_none() {
return Err("increment would overflow integer boundary".to_string());
}
Ok(vec![CounterEvent::Incremented { amount }])
}
CounterCommand::Decrement { amount } => {
if_dbg!(amount <= 0);
if amount <= 0 {
return Err("amount to decrement must be positive".to_string());
}
if self.value.checked_sub(amount).is_none() {
return Err("decrement would underflow integer boundary".to_string());
}
Ok(vec![CounterEvent::Decremented { amount }])
}
CounterCommand::Reset => {
Ok(vec![CounterEvent::ResetPerformed { value: 0 }])
}
}
}
/// Rebuilds aggregate state by replaying envelopes sequentially.
fn replay(events: &[ddd_cqrs_es::EventEnvelope<Self::Event, Self::Id>]) -> ddd_cqrs_es::LoadedAggregate<Self> {
let mut state = Self::new();
let mut revision = ddd_cqrs_es::INITIAL_REVISION;
if let Some(first) = events.first() {
state.id = first.aggregate_id.clone();
}
for envelope in events {
state.apply(&envelope.payload);
revision = envelope.revision;
}
ddd_cqrs_es::LoadedAggregate { state, revision }
}
}