use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{de, Serialize};
use std::error::Error as StdError;
use std::fmt::Debug;
use thiserror::Error;
pub trait AggregateId:
std::fmt::Display + Debug + Clone + Serialize + for<'de> de::Deserialize<'de> + Send + Sync + 'static {
fn type_name(&self) -> String;
fn value(&self) -> String;
}
pub trait Event: Debug + Clone + Serialize + for<'de> de::Deserialize<'de> + Send + Sync + 'static {
type ID: std::fmt::Display;
type AggregateID: AggregateId;
fn id(&self) -> &Self::ID;
fn aggregate_id(&self) -> &Self::AggregateID;
fn seq_nr(&self) -> usize;
fn occurred_at(&self) -> &DateTime<Utc>;
fn is_created(&self) -> bool;
}
pub trait Aggregate: Debug + Clone + Serialize + for<'de> de::Deserialize<'de> + Send + Sync + 'static {
type ID: AggregateId;
fn id(&self) -> &Self::ID;
fn seq_nr(&self) -> usize;
fn version(&self) -> usize;
fn set_version(&mut self, version: usize);
fn last_updated_at(&self) -> &DateTime<Utc>;
}
#[async_trait]
pub trait EventStore: Debug + Clone + Sync + Send + 'static {
type EV: Event;
type AG: Aggregate;
type AID: AggregateId;
async fn persist_event(&mut self, event: &Self::EV, version: usize) -> Result<(), EventStoreWriteError>;
async fn persist_event_and_snapshot(
&mut self,
event: &Self::EV,
aggregate: &Self::AG,
) -> Result<(), EventStoreWriteError>;
async fn get_latest_snapshot_by_id(&self, aid: &Self::AID) -> Result<Option<Self::AG>, EventStoreReadError>;
async fn get_events_by_id_since_seq_nr(
&self,
aid: &Self::AID,
seq_nr: usize,
) -> Result<Vec<Self::EV>, EventStoreReadError>;
}
pub(crate) fn format_optimistic_lock_message(
aid: &str,
expected_version: usize,
actual_version: Option<usize>,
) -> String {
match actual_version {
Some(actual) => format!(
"optimistic lock failed, aid={}, expected_version={}, actual_version={}",
aid, expected_version, actual
),
None => format!(
"optimistic lock failed, aid={}, expected_version={}",
aid, expected_version
),
}
}
#[derive(Error, Debug)]
pub enum EventStoreWriteError {
#[error("SerializeError: {0}")]
SerializationError(Box<dyn StdError + Send + Sync>),
#[error("OptimisticLockError: {0}")]
OptimisticLockError(String),
#[error("IOError: {0}")]
IOError(#[from] Box<dyn StdError + Send + Sync>),
#[error("OtherError: {0}")]
OtherError(String),
}
#[derive(Error, Debug)]
pub enum EventStoreReadError {
#[error("DeserializeError: {0}")]
DeserializationError(Box<dyn StdError + Send + Sync>),
#[error("IOError: {0}")]
IOError(#[from] Box<dyn StdError + Send + Sync>),
#[error("OtherError: {0}")]
OtherError(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_optimistic_lock_message_basic() {
let message = format_optimistic_lock_message("UserAccount-01H42K4ABWQ5V2XQEP3A48VE0Z", 3, None);
assert_eq!(
message,
"optimistic lock failed, aid=UserAccount-01H42K4ABWQ5V2XQEP3A48VE0Z, expected_version=3"
);
}
#[test]
fn test_optimistic_lock_message_with_actual_version() {
let message = format_optimistic_lock_message("UserAccount-01H42K4ABWQ5V2XQEP3A48VE0Z", 3, Some(4));
assert_eq!(
message,
"optimistic lock failed, aid=UserAccount-01H42K4ABWQ5V2XQEP3A48VE0Z, expected_version=3, actual_version=4"
);
}
#[test]
fn test_optimistic_lock_message_contains_only_aggregate_context() {
let message = format_optimistic_lock_message("aid-1", 1, Some(2));
let mut parts = message.split(", ");
assert_eq!(parts.next(), Some("optimistic lock failed"));
let allowed_keys = ["aid", "expected_version", "actual_version"];
for part in parts {
let key = part.split('=').next().unwrap();
assert!(allowed_keys.contains(&key), "unexpected field in message: {}", part);
}
assert!(!message.contains("://"), "message must not contain connection strings");
}
}