use std::sync::Arc;
use crate::{
Clock, EventCounterConfig, EventStore, IntervalConfig, Result, Storage, SystemClock, TimeUnit,
};
pub struct EventStoreBuilder {
clock: Option<Arc<dyn Clock>>,
storage: Option<Box<dyn Storage>>,
formatter: Option<Arc<dyn crate::Formatter>>,
configs: Vec<IntervalConfig>,
#[cfg(feature = "tokio")]
auto_persist_interval: Option<chrono::Duration>,
}
impl EventStoreBuilder {
pub(crate) fn new() -> Self {
Self {
clock: None,
storage: None,
formatter: None,
configs: Vec::new(),
#[cfg(feature = "tokio")]
auto_persist_interval: None,
}
}
pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
self.clock = Some(clock);
self
}
pub fn with_storage(mut self, storage: impl Storage + 'static) -> Self {
self.storage = Some(Box::new(storage));
self
}
pub fn with_format(mut self, formatter: impl crate::Formatter + 'static) -> Self {
self.formatter = Some(Arc::new(formatter));
self
}
pub fn track_seconds(mut self, count: usize) -> Self {
self.configs
.push(IntervalConfig::new_unchecked(count, TimeUnit::Seconds));
self
}
pub fn track_minutes(mut self, count: usize) -> Self {
self.configs
.push(IntervalConfig::new_unchecked(count, TimeUnit::Minutes));
self
}
pub fn track_hours(mut self, count: usize) -> Self {
self.configs
.push(IntervalConfig::new_unchecked(count, TimeUnit::Hours));
self
}
pub fn track_days(mut self, count: usize) -> Self {
self.configs
.push(IntervalConfig::new_unchecked(count, TimeUnit::Days));
self
}
pub fn track_weeks(mut self, count: usize) -> Self {
self.configs
.push(IntervalConfig::new_unchecked(count, TimeUnit::Weeks));
self
}
pub fn track_months(mut self, count: usize) -> Self {
self.configs
.push(IntervalConfig::new_unchecked(count, TimeUnit::Months));
self
}
pub fn track_years(mut self, count: usize) -> Self {
self.configs
.push(IntervalConfig::new_unchecked(count, TimeUnit::Years));
self
}
pub fn for_rate_limiting(mut self) -> Self {
self.configs = vec![
IntervalConfig::new_unchecked(60, TimeUnit::Minutes),
IntervalConfig::new_unchecked(72, TimeUnit::Hours),
];
self
}
pub fn for_analytics(mut self) -> Self {
self.configs = vec![
IntervalConfig::new_unchecked(56, TimeUnit::Days),
IntervalConfig::new_unchecked(52, TimeUnit::Weeks),
IntervalConfig::new_unchecked(12, TimeUnit::Months),
];
self
}
#[cfg(feature = "tokio")]
pub fn auto_persist(mut self, interval: chrono::Duration) -> Self {
self.auto_persist_interval = Some(interval);
self
}
pub fn build(self) -> Result<EventStore> {
#[cfg(feature = "tokio")]
if let Some(interval) = self.auto_persist_interval {
if interval.num_milliseconds() <= 0 {
return Err(crate::error::Error::InvalidAutoPersistInterval(interval));
}
if self.storage.is_none() {
return Err(crate::error::Error::AutoPersistRequiresStorage);
}
}
let clock = self.clock.unwrap_or_else(|| SystemClock::new());
let storage = self.storage;
let formatter = if storage.is_some() {
match self.formatter {
Some(f) => Some(f),
None => {
#[cfg(feature = "serde-bincode")]
{
Some(Arc::new(crate::formatter::BincodeFormat) as Arc<dyn crate::Formatter>)
}
#[cfg(all(feature = "serde-json", not(feature = "serde-bincode")))]
{
Some(Arc::new(crate::formatter::JsonFormat) as Arc<dyn crate::Formatter>)
}
#[cfg(not(any(feature = "serde-bincode", feature = "serde-json")))]
{
return Err(crate::error::Error::NoFormatterForStorage);
}
}
}
} else {
None
};
let config = if self.configs.is_empty() {
EventCounterConfig::default()
} else {
for config in &self.configs {
config.validate()?;
}
EventCounterConfig::new(self.configs)
};
#[allow(unused_mut)] let mut base_store = super::EventStore::from_parts(clock, storage, formatter, config);
#[cfg(feature = "tokio")]
{
if let Some(_interval) = self.auto_persist_interval {
let handle =
super::EventStore::spawn_auto_persist(base_store.inner.clone(), _interval);
base_store.auto_persist_handle = Some(handle);
}
}
Ok(base_store)
}
}
impl Default for EventStoreBuilder {
fn default() -> Self {
Self::new()
}
}
impl EventStore {
pub fn builder() -> EventStoreBuilder {
EventStoreBuilder::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::MemoryStorage;
use crate::TestClock;
#[test]
fn test_new_builder() {
let builder = EventStoreBuilder::new();
assert!(builder.clock.is_none());
assert!(builder.storage.is_none());
assert_eq!(builder.configs.len(), 0);
}
#[test]
fn test_default_builder() {
let builder = EventStoreBuilder::default();
assert!(builder.clock.is_none());
assert!(builder.storage.is_none());
assert_eq!(builder.configs.len(), 0);
}
#[test]
fn test_with_clock() {
let clock = TestClock::new();
let builder = EventStoreBuilder::new().with_clock(clock);
assert!(builder.clock.is_some());
}
#[test]
fn test_with_storage() {
let storage = MemoryStorage::new();
let builder = EventStoreBuilder::new().with_storage(storage);
assert!(builder.storage.is_some());
}
#[test]
fn test_track_minutes() {
let builder = EventStoreBuilder::new().track_minutes(60);
assert_eq!(builder.configs.len(), 1);
assert_eq!(builder.configs[0].bucket_count(), 60);
assert_eq!(builder.configs[0].time_unit(), TimeUnit::Minutes);
}
#[test]
fn test_track_hours() {
let builder = EventStoreBuilder::new().track_hours(72);
assert_eq!(builder.configs.len(), 1);
assert_eq!(builder.configs[0].bucket_count(), 72);
assert_eq!(builder.configs[0].time_unit(), TimeUnit::Hours);
}
#[test]
fn test_track_days() {
let builder = EventStoreBuilder::new().track_days(56);
assert_eq!(builder.configs.len(), 1);
assert_eq!(builder.configs[0].bucket_count(), 56);
assert_eq!(builder.configs[0].time_unit(), TimeUnit::Days);
}
#[test]
fn test_track_weeks() {
let builder = EventStoreBuilder::new().track_weeks(52);
assert_eq!(builder.configs.len(), 1);
assert_eq!(builder.configs[0].bucket_count(), 52);
assert_eq!(builder.configs[0].time_unit(), TimeUnit::Weeks);
}
#[test]
fn test_track_months() {
let builder = EventStoreBuilder::new().track_months(12);
assert_eq!(builder.configs.len(), 1);
assert_eq!(builder.configs[0].bucket_count(), 12);
assert_eq!(builder.configs[0].time_unit(), TimeUnit::Months);
}
#[test]
fn test_track_years() {
let builder = EventStoreBuilder::new().track_years(4);
assert_eq!(builder.configs.len(), 1);
assert_eq!(builder.configs[0].bucket_count(), 4);
assert_eq!(builder.configs[0].time_unit(), TimeUnit::Years);
}
#[test]
fn test_for_rate_limiting_preset() {
let builder = EventStoreBuilder::new().for_rate_limiting();
assert_eq!(builder.configs.len(), 2);
assert_eq!(builder.configs[0].bucket_count(), 60);
assert_eq!(builder.configs[0].time_unit(), TimeUnit::Minutes);
assert_eq!(builder.configs[1].bucket_count(), 72);
assert_eq!(builder.configs[1].time_unit(), TimeUnit::Hours);
}
#[test]
fn test_for_analytics_preset() {
let builder = EventStoreBuilder::new().for_analytics();
assert_eq!(builder.configs.len(), 3);
assert_eq!(builder.configs[0].bucket_count(), 56);
assert_eq!(builder.configs[0].time_unit(), TimeUnit::Days);
assert_eq!(builder.configs[1].bucket_count(), 52);
assert_eq!(builder.configs[1].time_unit(), TimeUnit::Weeks);
assert_eq!(builder.configs[2].bucket_count(), 12);
assert_eq!(builder.configs[2].time_unit(), TimeUnit::Months);
}
#[test]
fn test_build_default() {
let store = EventStoreBuilder::new().build().unwrap();
let intervals = store.tracked_intervals();
assert_eq!(intervals.len(), 6);
assert!(intervals.contains(&(TimeUnit::Minutes, 60)));
assert!(intervals.contains(&(TimeUnit::Hours, 72)));
assert!(intervals.contains(&(TimeUnit::Days, 56)));
assert!(intervals.contains(&(TimeUnit::Weeks, 52)));
assert!(intervals.contains(&(TimeUnit::Months, 12)));
assert!(intervals.contains(&(TimeUnit::Years, 4)));
}
#[test]
fn test_build_with_custom_config() {
let store = EventStoreBuilder::new()
.track_minutes(30)
.track_hours(24)
.build()
.unwrap();
let intervals = store.tracked_intervals();
assert_eq!(intervals.len(), 2);
assert!(intervals.contains(&(TimeUnit::Minutes, 30)));
assert!(intervals.contains(&(TimeUnit::Hours, 24)));
}
#[test]
fn test_build_with_rate_limiting_preset() {
let store = EventStoreBuilder::new()
.for_rate_limiting()
.build()
.unwrap();
let intervals = store.tracked_intervals();
assert_eq!(intervals.len(), 2);
assert!(intervals.contains(&(TimeUnit::Minutes, 60)));
assert!(intervals.contains(&(TimeUnit::Hours, 72)));
}
#[test]
fn test_build_with_analytics_preset() {
let store = EventStoreBuilder::new().for_analytics().build().unwrap();
let intervals = store.tracked_intervals();
assert_eq!(intervals.len(), 3);
assert!(intervals.contains(&(TimeUnit::Days, 56)));
assert!(intervals.contains(&(TimeUnit::Weeks, 52)));
assert!(intervals.contains(&(TimeUnit::Months, 12)));
}
#[test]
fn test_build_with_clock() {
let test_clock = TestClock::new();
let _expected_time = test_clock.now();
let store = EventStoreBuilder::new()
.with_clock(test_clock)
.build()
.unwrap();
store.record("test");
assert_eq!(store.query("test").last_days(1).sum(), Some(1));
}
#[test]
#[cfg(any(feature = "serde-bincode", feature = "serde-json"))]
fn test_build_with_storage() {
let storage = MemoryStorage::new();
let store = EventStoreBuilder::new()
.with_storage(storage)
.build()
.unwrap();
assert_eq!(store.tracked_intervals().len(), 6);
}
#[test]
#[cfg(any(feature = "serde-bincode", feature = "serde-json"))]
fn test_build_with_all_options() {
let test_clock = TestClock::new();
let storage = MemoryStorage::new();
let store = EventStoreBuilder::new()
.with_clock(test_clock)
.with_storage(storage)
.track_minutes(120)
.track_hours(48)
.build()
.unwrap();
let intervals = store.tracked_intervals();
assert_eq!(intervals.len(), 2);
assert!(intervals.contains(&(TimeUnit::Minutes, 120)));
assert!(intervals.contains(&(TimeUnit::Hours, 48)));
}
#[test]
fn test_event_store_builder_method() {
let builder = EventStore::builder();
assert!(builder.clock.is_none());
assert!(builder.storage.is_none());
assert_eq!(builder.configs.len(), 0);
}
#[test]
fn test_builder_fluent_api() {
let store = EventStore::builder()
.track_minutes(60)
.track_hours(24)
.track_days(7)
.build()
.unwrap();
let intervals = store.tracked_intervals();
assert_eq!(intervals.len(), 3);
}
#[test]
fn test_preset_overwrites_previous_config() {
let builder = EventStoreBuilder::new()
.track_minutes(30)
.track_hours(12)
.for_rate_limiting();
assert_eq!(builder.configs.len(), 2);
assert_eq!(builder.configs[0].bucket_count(), 60);
assert_eq!(builder.configs[1].bucket_count(), 72);
}
#[test]
fn test_custom_config_after_preset() {
let builder = EventStoreBuilder::new().for_rate_limiting().track_days(30);
assert_eq!(builder.configs.len(), 3);
assert_eq!(builder.configs[0].time_unit(), TimeUnit::Minutes);
assert_eq!(builder.configs[1].time_unit(), TimeUnit::Hours);
assert_eq!(builder.configs[2].time_unit(), TimeUnit::Days);
}
#[test]
fn test_track_minutes_rejects_zero() {
let result = EventStoreBuilder::new().track_minutes(0).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_minutes_rejects_too_large() {
let result = EventStoreBuilder::new().track_minutes(100_001).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_hours_rejects_zero() {
let result = EventStoreBuilder::new().track_hours(0).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_hours_rejects_too_large() {
let result = EventStoreBuilder::new().track_hours(100_001).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_days_rejects_zero() {
let result = EventStoreBuilder::new().track_days(0).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_days_rejects_too_large() {
let result = EventStoreBuilder::new().track_days(100_001).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_weeks_rejects_zero() {
let result = EventStoreBuilder::new().track_weeks(0).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_weeks_rejects_too_large() {
let result = EventStoreBuilder::new().track_weeks(100_001).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_months_rejects_zero() {
let result = EventStoreBuilder::new().track_months(0).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_months_rejects_too_large() {
let result = EventStoreBuilder::new().track_months(100_001).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_years_rejects_zero() {
let result = EventStoreBuilder::new().track_years(0).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_years_rejects_too_large() {
let result = EventStoreBuilder::new().track_years(100_001).build();
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("bucket count"));
}
}
#[test]
fn test_track_minutes_accepts_valid_values() {
let result = EventStoreBuilder::new().track_minutes(1).build();
assert!(result.is_ok());
let result = EventStoreBuilder::new().track_minutes(100_000).build();
assert!(result.is_ok());
}
#[test]
fn test_track_hours_accepts_valid_values() {
let result = EventStoreBuilder::new().track_hours(1).build();
assert!(result.is_ok());
let result = EventStoreBuilder::new().track_hours(100_000).build();
assert!(result.is_ok());
}
#[cfg(feature = "tokio")]
#[test]
fn test_auto_persist_method_exists() {
use chrono::Duration;
let builder = EventStoreBuilder::new().auto_persist(Duration::seconds(60));
assert!(builder.auto_persist_interval.is_some());
assert_eq!(
builder.auto_persist_interval.unwrap(),
Duration::seconds(60)
);
}
#[cfg(all(feature = "tokio", feature = "serde"))]
#[tokio::test]
async fn test_unified_build_without_auto_persist() {
let store = EventStoreBuilder::new()
.with_storage(MemoryStorage::new())
.build()
.unwrap();
store.record("test");
assert_eq!(store.query("test").last_days(1).sum(), Some(1));
}
#[cfg(all(feature = "tokio", feature = "serde"))]
#[tokio::test]
async fn test_unified_build_with_auto_persist() {
use chrono::Duration;
let store = EventStoreBuilder::new()
.with_storage(MemoryStorage::new())
.auto_persist(Duration::milliseconds(50))
.build()
.unwrap();
store.record("test");
assert_eq!(store.query("test").last_days(1).sum(), Some(1));
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
assert!(!store.is_dirty());
}
#[cfg(all(feature = "tokio", feature = "serde"))]
#[tokio::test]
async fn test_unified_build_auto_persist_no_locking_needed() {
use chrono::Duration;
let store = EventStoreBuilder::new()
.with_storage(MemoryStorage::new())
.auto_persist(Duration::milliseconds(50))
.build()
.unwrap();
store.record("event1");
store.record("event2");
store.record("event3");
assert_eq!(store.query("event1").last_days(1).sum(), Some(1));
assert_eq!(store.query("event2").last_days(1).sum(), Some(1));
assert_eq!(store.query("event3").last_days(1).sum(), Some(1));
}
#[cfg(feature = "serde-bincode")]
#[test]
fn test_with_format_bincode() {
use crate::formatter::BincodeFormat;
let builder = EventStoreBuilder::new()
.with_storage(MemoryStorage::new())
.with_format(BincodeFormat);
assert!(builder.formatter.is_some());
}
#[cfg(feature = "serde-json")]
#[test]
fn test_with_format_json() {
use crate::formatter::JsonFormat;
let builder = EventStoreBuilder::new()
.with_storage(MemoryStorage::new())
.with_format(JsonFormat);
assert!(builder.formatter.is_some());
}
#[cfg(feature = "serde-bincode")]
#[test]
fn test_default_formatter_is_bincode() {
let store = EventStoreBuilder::new()
.with_storage(MemoryStorage::new())
.build()
.unwrap();
store.record("test");
assert_eq!(store.query("test").last_days(1).sum(), Some(1));
}
#[cfg(all(feature = "serde-bincode", feature = "serde"))]
#[test]
fn test_formatter_used_in_persistence() {
use crate::formatter::BincodeFormat;
let store = EventStoreBuilder::new()
.with_storage(MemoryStorage::new())
.with_format(BincodeFormat)
.build()
.unwrap();
store.record("event1");
store.record("event2");
let result = store.persist();
assert!(result.is_ok());
}
#[cfg(all(feature = "serde-json", feature = "serde"))]
#[test]
fn test_json_formatter_used_in_persistence() {
use crate::formatter::JsonFormat;
let store = EventStoreBuilder::new()
.with_storage(MemoryStorage::new())
.with_format(JsonFormat)
.build()
.unwrap();
store.record("event1");
store.record("event2");
let result = store.persist();
assert!(result.is_ok());
}
#[test]
#[cfg(feature = "tokio")]
fn test_auto_persist_negative_interval_fails() {
use chrono::Duration;
let result = EventStoreBuilder::new()
.with_storage(MemoryStorage::new())
.auto_persist(Duration::seconds(-60))
.build();
assert!(result.is_err());
match result {
Err(crate::error::Error::InvalidAutoPersistInterval(d)) => {
assert_eq!(d.num_seconds(), -60);
}
_ => panic!("Expected InvalidAutoPersistInterval error"),
}
}
#[test]
#[cfg(feature = "tokio")]
fn test_auto_persist_zero_interval_fails() {
use chrono::Duration;
let result = EventStoreBuilder::new()
.with_storage(MemoryStorage::new())
.auto_persist(Duration::zero())
.build();
assert!(result.is_err());
match result {
Err(crate::error::Error::InvalidAutoPersistInterval(d)) => {
assert!(d.num_milliseconds() <= 0);
}
_ => panic!("Expected InvalidAutoPersistInterval error"),
}
}
}