tiny_counter/store/
builder.rs

1/// Builder pattern for EventStore configuration.
2use std::sync::Arc;
3
4use crate::{
5    Clock, EventCounterConfig, EventStore, IntervalConfig, Result, Storage, SystemClock, TimeUnit,
6};
7
8/// Builder for creating an EventStore with custom configuration.
9///
10/// The builder provides a fluent interface for configuring:
11/// - Custom clock implementations (for testing or other purposes)
12/// - Storage backends (for persistence)
13/// - Time interval tracking (minutes, hours, days, weeks, months, years)
14/// - Preset configurations (for rate limiting or analytics)
15///
16/// # Examples
17///
18/// ```
19/// use tiny_counter::EventStore;
20///
21/// let store = EventStore::builder()
22///     .for_rate_limiting()
23///     .build()
24///     .unwrap();
25/// ```
26pub struct EventStoreBuilder {
27    clock: Option<Arc<dyn Clock>>,
28    storage: Option<Box<dyn Storage>>,
29    formatter: Option<Arc<dyn crate::Formatter>>,
30    configs: Vec<IntervalConfig>,
31    #[cfg(feature = "tokio")]
32    auto_persist_interval: Option<chrono::Duration>,
33}
34
35impl EventStoreBuilder {
36    /// Creates a new builder with no configuration.
37    pub(crate) fn new() -> Self {
38        Self {
39            clock: None,
40            storage: None,
41            formatter: None,
42            configs: Vec::new(),
43            #[cfg(feature = "tokio")]
44            auto_persist_interval: None,
45        }
46    }
47
48    /// Sets a custom clock implementation.
49    ///
50    /// By default, SystemClock is used.
51    ///
52    /// Accepts either a concrete Clock implementation or an `Arc<dyn Clock>`.
53    pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
54        self.clock = Some(clock);
55        self
56    }
57
58    /// Sets a storage backend for persistence.
59    ///
60    /// By default, no storage is configured.
61    pub fn with_storage(mut self, storage: impl Storage + 'static) -> Self {
62        self.storage = Some(Box::new(storage));
63        self
64    }
65
66    /// Sets a serialization format for persistence.
67    ///
68    /// By default, BincodeFormat is used if available.
69    pub fn with_format(mut self, formatter: impl crate::Formatter + 'static) -> Self {
70        self.formatter = Some(Arc::new(formatter));
71        self
72    }
73
74    /// Adds second-level tracking with the specified bucket count.
75    pub fn track_seconds(mut self, count: usize) -> Self {
76        self.configs
77            .push(IntervalConfig::new_unchecked(count, TimeUnit::Seconds));
78        self
79    }
80
81    /// Adds minute-level tracking with the specified bucket count.
82    pub fn track_minutes(mut self, count: usize) -> Self {
83        self.configs
84            .push(IntervalConfig::new_unchecked(count, TimeUnit::Minutes));
85        self
86    }
87
88    /// Adds hour-level tracking with the specified bucket count.
89    pub fn track_hours(mut self, count: usize) -> Self {
90        self.configs
91            .push(IntervalConfig::new_unchecked(count, TimeUnit::Hours));
92        self
93    }
94
95    /// Adds day-level tracking with the specified bucket count.
96    pub fn track_days(mut self, count: usize) -> Self {
97        self.configs
98            .push(IntervalConfig::new_unchecked(count, TimeUnit::Days));
99        self
100    }
101
102    /// Adds week-level tracking with the specified bucket count.
103    pub fn track_weeks(mut self, count: usize) -> Self {
104        self.configs
105            .push(IntervalConfig::new_unchecked(count, TimeUnit::Weeks));
106        self
107    }
108
109    /// Adds month-level tracking with the specified bucket count.
110    pub fn track_months(mut self, count: usize) -> Self {
111        self.configs
112            .push(IntervalConfig::new_unchecked(count, TimeUnit::Months));
113        self
114    }
115
116    /// Adds year-level tracking with the specified bucket count.
117    pub fn track_years(mut self, count: usize) -> Self {
118        self.configs
119            .push(IntervalConfig::new_unchecked(count, TimeUnit::Years));
120        self
121    }
122
123    /// Configures the store for rate limiting use cases.
124    ///
125    /// Tracks: 60 minutes, 72 hours (short-term tracking)
126    pub fn for_rate_limiting(mut self) -> Self {
127        self.configs = vec![
128            IntervalConfig::new_unchecked(60, TimeUnit::Minutes),
129            IntervalConfig::new_unchecked(72, TimeUnit::Hours),
130        ];
131        self
132    }
133
134    /// Configures the store for analytics use cases.
135    ///
136    /// Tracks: 56 days, 52 weeks, 12 months (long-term tracking)
137    pub fn for_analytics(mut self) -> Self {
138        self.configs = vec![
139            IntervalConfig::new_unchecked(56, TimeUnit::Days),
140            IntervalConfig::new_unchecked(52, TimeUnit::Weeks),
141            IntervalConfig::new_unchecked(12, TimeUnit::Months),
142        ];
143        self
144    }
145
146    /// Enables automatic background persistence with the specified interval.
147    ///
148    /// Requires the `tokio` feature to be enabled, storage to be configured, and a formatter to be available.
149    /// A background task will periodically check if the store is dirty and persist
150    /// if needed.
151    ///
152    /// # Examples
153    ///
154    /// ```rust,no_run
155    /// # use tiny_counter::EventStore;
156    /// # use tiny_counter::storage::MemoryStorage;
157    /// use chrono::Duration;
158    ///
159    /// let store = EventStore::builder()
160    ///     .with_storage(MemoryStorage::new())
161    ///     .auto_persist(Duration::seconds(60))
162    ///     .build()
163    ///     .unwrap();
164    /// ```
165    #[cfg(feature = "tokio")]
166    pub fn auto_persist(mut self, interval: chrono::Duration) -> Self {
167        self.auto_persist_interval = Some(interval);
168        self
169    }
170
171    /// Builds the EventStore with the configured settings.
172    ///
173    /// If `auto_persist()` was called with the tokio feature enabled,
174    /// a background task will be spawned to automatically persist dirty state.
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if:
179    /// - Storage is configured but loading existing data fails
180    /// - Any bucket count is invalid
181    /// - `auto_persist()` interval is not positive (zero or negative)
182    /// - `auto_persist()` is set but storage is not configured
183    /// - Storage is configured but no formatter is available
184    pub fn build(self) -> Result<EventStore> {
185        // Validate auto_persist interval if configured
186        #[cfg(feature = "tokio")]
187        if let Some(interval) = self.auto_persist_interval {
188            if interval.num_milliseconds() <= 0 {
189                return Err(crate::error::Error::InvalidAutoPersistInterval(interval));
190            }
191            // auto_persist requires storage to be configured
192            if self.storage.is_none() {
193                return Err(crate::error::Error::AutoPersistRequiresStorage);
194            }
195        }
196
197        let clock = self.clock.unwrap_or_else(|| SystemClock::new());
198        let storage = self.storage;
199
200        // Determine formatter if storage is configured
201        let formatter = if storage.is_some() {
202            match self.formatter {
203                Some(f) => Some(f),
204                None => {
205                    // Try to default to an available formatter
206                    #[cfg(feature = "serde-bincode")]
207                    {
208                        Some(Arc::new(crate::formatter::BincodeFormat) as Arc<dyn crate::Formatter>)
209                    }
210                    #[cfg(all(feature = "serde-json", not(feature = "serde-bincode")))]
211                    {
212                        Some(Arc::new(crate::formatter::JsonFormat) as Arc<dyn crate::Formatter>)
213                    }
214                    #[cfg(not(any(feature = "serde-bincode", feature = "serde-json")))]
215                    {
216                        return Err(crate::error::Error::NoFormatterForStorage);
217                    }
218                }
219            }
220        } else {
221            None
222        };
223
224        let config = if self.configs.is_empty() {
225            // Use default configuration
226            EventCounterConfig::default()
227        } else {
228            // Validate all bucket counts
229            for config in &self.configs {
230                config.validate()?;
231            }
232            EventCounterConfig::new(self.configs)
233        };
234
235        // Create the base EventStore using the internal constructor
236        #[allow(unused_mut)] // mut needed when tokio feature enabled
237        let mut base_store = super::EventStore::from_parts(clock, storage, formatter, config);
238
239        // If tokio feature enabled and auto_persist configured, spawn background task
240        #[cfg(feature = "tokio")]
241        {
242            if let Some(_interval) = self.auto_persist_interval {
243                let handle =
244                    super::EventStore::spawn_auto_persist(base_store.inner.clone(), _interval);
245                base_store.auto_persist_handle = Some(handle);
246            }
247        }
248
249        // Return EventStore (which is now just BaseEventStore)
250        Ok(base_store)
251    }
252}
253
254impl Default for EventStoreBuilder {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260impl EventStore {
261    /// Creates a builder for configuring an EventStore.
262    pub fn builder() -> EventStoreBuilder {
263        EventStoreBuilder::new()
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    use crate::storage::MemoryStorage;
272    use crate::TestClock;
273
274    #[test]
275    fn test_new_builder() {
276        let builder = EventStoreBuilder::new();
277        assert!(builder.clock.is_none());
278        assert!(builder.storage.is_none());
279        assert_eq!(builder.configs.len(), 0);
280    }
281
282    #[test]
283    fn test_default_builder() {
284        let builder = EventStoreBuilder::default();
285        assert!(builder.clock.is_none());
286        assert!(builder.storage.is_none());
287        assert_eq!(builder.configs.len(), 0);
288    }
289
290    #[test]
291    fn test_with_clock() {
292        let clock = TestClock::new();
293        let builder = EventStoreBuilder::new().with_clock(clock);
294        assert!(builder.clock.is_some());
295    }
296
297    #[test]
298    fn test_with_storage() {
299        let storage = MemoryStorage::new();
300        let builder = EventStoreBuilder::new().with_storage(storage);
301        assert!(builder.storage.is_some());
302    }
303
304    #[test]
305    fn test_track_minutes() {
306        let builder = EventStoreBuilder::new().track_minutes(60);
307        assert_eq!(builder.configs.len(), 1);
308        assert_eq!(builder.configs[0].bucket_count(), 60);
309        assert_eq!(builder.configs[0].time_unit(), TimeUnit::Minutes);
310    }
311
312    #[test]
313    fn test_track_hours() {
314        let builder = EventStoreBuilder::new().track_hours(72);
315        assert_eq!(builder.configs.len(), 1);
316        assert_eq!(builder.configs[0].bucket_count(), 72);
317        assert_eq!(builder.configs[0].time_unit(), TimeUnit::Hours);
318    }
319
320    #[test]
321    fn test_track_days() {
322        let builder = EventStoreBuilder::new().track_days(56);
323        assert_eq!(builder.configs.len(), 1);
324        assert_eq!(builder.configs[0].bucket_count(), 56);
325        assert_eq!(builder.configs[0].time_unit(), TimeUnit::Days);
326    }
327
328    #[test]
329    fn test_track_weeks() {
330        let builder = EventStoreBuilder::new().track_weeks(52);
331        assert_eq!(builder.configs.len(), 1);
332        assert_eq!(builder.configs[0].bucket_count(), 52);
333        assert_eq!(builder.configs[0].time_unit(), TimeUnit::Weeks);
334    }
335
336    #[test]
337    fn test_track_months() {
338        let builder = EventStoreBuilder::new().track_months(12);
339        assert_eq!(builder.configs.len(), 1);
340        assert_eq!(builder.configs[0].bucket_count(), 12);
341        assert_eq!(builder.configs[0].time_unit(), TimeUnit::Months);
342    }
343
344    #[test]
345    fn test_track_years() {
346        let builder = EventStoreBuilder::new().track_years(4);
347        assert_eq!(builder.configs.len(), 1);
348        assert_eq!(builder.configs[0].bucket_count(), 4);
349        assert_eq!(builder.configs[0].time_unit(), TimeUnit::Years);
350    }
351
352    #[test]
353    fn test_for_rate_limiting_preset() {
354        let builder = EventStoreBuilder::new().for_rate_limiting();
355        assert_eq!(builder.configs.len(), 2);
356
357        // 60 minutes
358        assert_eq!(builder.configs[0].bucket_count(), 60);
359        assert_eq!(builder.configs[0].time_unit(), TimeUnit::Minutes);
360
361        // 72 hours
362        assert_eq!(builder.configs[1].bucket_count(), 72);
363        assert_eq!(builder.configs[1].time_unit(), TimeUnit::Hours);
364    }
365
366    #[test]
367    fn test_for_analytics_preset() {
368        let builder = EventStoreBuilder::new().for_analytics();
369        assert_eq!(builder.configs.len(), 3);
370
371        // 56 days
372        assert_eq!(builder.configs[0].bucket_count(), 56);
373        assert_eq!(builder.configs[0].time_unit(), TimeUnit::Days);
374
375        // 52 weeks
376        assert_eq!(builder.configs[1].bucket_count(), 52);
377        assert_eq!(builder.configs[1].time_unit(), TimeUnit::Weeks);
378
379        // 12 months
380        assert_eq!(builder.configs[2].bucket_count(), 12);
381        assert_eq!(builder.configs[2].time_unit(), TimeUnit::Months);
382    }
383
384    #[test]
385    fn test_build_default() {
386        let store = EventStoreBuilder::new().build().unwrap();
387
388        // Default configuration has 6 time units
389        let intervals = store.tracked_intervals();
390        assert_eq!(intervals.len(), 6);
391
392        // Verify default configuration
393        assert!(intervals.contains(&(TimeUnit::Minutes, 60)));
394        assert!(intervals.contains(&(TimeUnit::Hours, 72)));
395        assert!(intervals.contains(&(TimeUnit::Days, 56)));
396        assert!(intervals.contains(&(TimeUnit::Weeks, 52)));
397        assert!(intervals.contains(&(TimeUnit::Months, 12)));
398        assert!(intervals.contains(&(TimeUnit::Years, 4)));
399    }
400
401    #[test]
402    fn test_build_with_custom_config() {
403        let store = EventStoreBuilder::new()
404            .track_minutes(30)
405            .track_hours(24)
406            .build()
407            .unwrap();
408
409        let intervals = store.tracked_intervals();
410        assert_eq!(intervals.len(), 2);
411        assert!(intervals.contains(&(TimeUnit::Minutes, 30)));
412        assert!(intervals.contains(&(TimeUnit::Hours, 24)));
413    }
414
415    #[test]
416    fn test_build_with_rate_limiting_preset() {
417        let store = EventStoreBuilder::new()
418            .for_rate_limiting()
419            .build()
420            .unwrap();
421
422        let intervals = store.tracked_intervals();
423        assert_eq!(intervals.len(), 2);
424        assert!(intervals.contains(&(TimeUnit::Minutes, 60)));
425        assert!(intervals.contains(&(TimeUnit::Hours, 72)));
426    }
427
428    #[test]
429    fn test_build_with_analytics_preset() {
430        let store = EventStoreBuilder::new().for_analytics().build().unwrap();
431
432        let intervals = store.tracked_intervals();
433        assert_eq!(intervals.len(), 3);
434        assert!(intervals.contains(&(TimeUnit::Days, 56)));
435        assert!(intervals.contains(&(TimeUnit::Weeks, 52)));
436        assert!(intervals.contains(&(TimeUnit::Months, 12)));
437    }
438
439    #[test]
440    fn test_build_with_clock() {
441        let test_clock = TestClock::new();
442        let _expected_time = test_clock.now();
443
444        let store = EventStoreBuilder::new()
445            .with_clock(test_clock)
446            .build()
447            .unwrap();
448
449        // Verify the clock is used (indirectly by checking that store works)
450        store.record("test");
451        assert_eq!(store.query("test").last_days(1).sum(), Some(1));
452    }
453
454    #[test]
455    #[cfg(any(feature = "serde-bincode", feature = "serde-json"))]
456    fn test_build_with_storage() {
457        let storage = MemoryStorage::new();
458        let store = EventStoreBuilder::new()
459            .with_storage(storage)
460            .build()
461            .unwrap();
462
463        // Storage is configured, so we should be able to persist
464        // We can't directly check storage, but we can verify the store was built
465        assert_eq!(store.tracked_intervals().len(), 6);
466    }
467
468    #[test]
469    #[cfg(any(feature = "serde-bincode", feature = "serde-json"))]
470    fn test_build_with_all_options() {
471        let test_clock = TestClock::new();
472        let storage = MemoryStorage::new();
473
474        let store = EventStoreBuilder::new()
475            .with_clock(test_clock)
476            .with_storage(storage)
477            .track_minutes(120)
478            .track_hours(48)
479            .build()
480            .unwrap();
481
482        let intervals = store.tracked_intervals();
483        assert_eq!(intervals.len(), 2);
484        assert!(intervals.contains(&(TimeUnit::Minutes, 120)));
485        assert!(intervals.contains(&(TimeUnit::Hours, 48)));
486    }
487
488    #[test]
489    fn test_event_store_builder_method() {
490        let builder = EventStore::builder();
491        assert!(builder.clock.is_none());
492        assert!(builder.storage.is_none());
493        assert_eq!(builder.configs.len(), 0);
494    }
495
496    #[test]
497    fn test_builder_fluent_api() {
498        // Test chaining multiple methods
499        let store = EventStore::builder()
500            .track_minutes(60)
501            .track_hours(24)
502            .track_days(7)
503            .build()
504            .unwrap();
505
506        let intervals = store.tracked_intervals();
507        assert_eq!(intervals.len(), 3);
508    }
509
510    #[test]
511    fn test_preset_overwrites_previous_config() {
512        // If you call a preset after setting custom intervals, it should replace them
513        let builder = EventStoreBuilder::new()
514            .track_minutes(30)
515            .track_hours(12)
516            .for_rate_limiting();
517
518        // Should have rate limiting config, not the custom one
519        assert_eq!(builder.configs.len(), 2);
520        assert_eq!(builder.configs[0].bucket_count(), 60);
521        assert_eq!(builder.configs[1].bucket_count(), 72);
522    }
523
524    #[test]
525    fn test_custom_config_after_preset() {
526        // You can add custom intervals after a preset
527        let builder = EventStoreBuilder::new().for_rate_limiting().track_days(30);
528
529        // Should have rate limiting + days
530        assert_eq!(builder.configs.len(), 3);
531        assert_eq!(builder.configs[0].time_unit(), TimeUnit::Minutes);
532        assert_eq!(builder.configs[1].time_unit(), TimeUnit::Hours);
533        assert_eq!(builder.configs[2].time_unit(), TimeUnit::Days);
534    }
535
536    // Validation tests for track_* methods
537    #[test]
538    fn test_track_minutes_rejects_zero() {
539        let result = EventStoreBuilder::new().track_minutes(0).build();
540        assert!(result.is_err());
541        if let Err(e) = result {
542            assert!(e.to_string().contains("bucket count"));
543        }
544    }
545
546    #[test]
547    fn test_track_minutes_rejects_too_large() {
548        let result = EventStoreBuilder::new().track_minutes(100_001).build();
549        assert!(result.is_err());
550        if let Err(e) = result {
551            assert!(e.to_string().contains("bucket count"));
552        }
553    }
554
555    #[test]
556    fn test_track_hours_rejects_zero() {
557        let result = EventStoreBuilder::new().track_hours(0).build();
558        assert!(result.is_err());
559        if let Err(e) = result {
560            assert!(e.to_string().contains("bucket count"));
561        }
562    }
563
564    #[test]
565    fn test_track_hours_rejects_too_large() {
566        let result = EventStoreBuilder::new().track_hours(100_001).build();
567        assert!(result.is_err());
568        if let Err(e) = result {
569            assert!(e.to_string().contains("bucket count"));
570        }
571    }
572
573    #[test]
574    fn test_track_days_rejects_zero() {
575        let result = EventStoreBuilder::new().track_days(0).build();
576        assert!(result.is_err());
577        if let Err(e) = result {
578            assert!(e.to_string().contains("bucket count"));
579        }
580    }
581
582    #[test]
583    fn test_track_days_rejects_too_large() {
584        let result = EventStoreBuilder::new().track_days(100_001).build();
585        assert!(result.is_err());
586        if let Err(e) = result {
587            assert!(e.to_string().contains("bucket count"));
588        }
589    }
590
591    #[test]
592    fn test_track_weeks_rejects_zero() {
593        let result = EventStoreBuilder::new().track_weeks(0).build();
594        assert!(result.is_err());
595        if let Err(e) = result {
596            assert!(e.to_string().contains("bucket count"));
597        }
598    }
599
600    #[test]
601    fn test_track_weeks_rejects_too_large() {
602        let result = EventStoreBuilder::new().track_weeks(100_001).build();
603        assert!(result.is_err());
604        if let Err(e) = result {
605            assert!(e.to_string().contains("bucket count"));
606        }
607    }
608
609    #[test]
610    fn test_track_months_rejects_zero() {
611        let result = EventStoreBuilder::new().track_months(0).build();
612        assert!(result.is_err());
613        if let Err(e) = result {
614            assert!(e.to_string().contains("bucket count"));
615        }
616    }
617
618    #[test]
619    fn test_track_months_rejects_too_large() {
620        let result = EventStoreBuilder::new().track_months(100_001).build();
621        assert!(result.is_err());
622        if let Err(e) = result {
623            assert!(e.to_string().contains("bucket count"));
624        }
625    }
626
627    #[test]
628    fn test_track_years_rejects_zero() {
629        let result = EventStoreBuilder::new().track_years(0).build();
630        assert!(result.is_err());
631        if let Err(e) = result {
632            assert!(e.to_string().contains("bucket count"));
633        }
634    }
635
636    #[test]
637    fn test_track_years_rejects_too_large() {
638        let result = EventStoreBuilder::new().track_years(100_001).build();
639        assert!(result.is_err());
640        if let Err(e) = result {
641            assert!(e.to_string().contains("bucket count"));
642        }
643    }
644
645    #[test]
646    fn test_track_minutes_accepts_valid_values() {
647        let result = EventStoreBuilder::new().track_minutes(1).build();
648        assert!(result.is_ok());
649
650        let result = EventStoreBuilder::new().track_minutes(100_000).build();
651        assert!(result.is_ok());
652    }
653
654    #[test]
655    fn test_track_hours_accepts_valid_values() {
656        let result = EventStoreBuilder::new().track_hours(1).build();
657        assert!(result.is_ok());
658
659        let result = EventStoreBuilder::new().track_hours(100_000).build();
660        assert!(result.is_ok());
661    }
662
663    #[cfg(feature = "tokio")]
664    #[test]
665    fn test_auto_persist_method_exists() {
666        use chrono::Duration;
667
668        let builder = EventStoreBuilder::new().auto_persist(Duration::seconds(60));
669
670        assert!(builder.auto_persist_interval.is_some());
671        assert_eq!(
672            builder.auto_persist_interval.unwrap(),
673            Duration::seconds(60)
674        );
675    }
676
677    // New tests for unified build() API
678    #[cfg(all(feature = "tokio", feature = "serde"))]
679    #[tokio::test]
680    async fn test_unified_build_without_auto_persist() {
681        let store = EventStoreBuilder::new()
682            .with_storage(MemoryStorage::new())
683            .build()
684            .unwrap();
685
686        // Should work normally
687        store.record("test");
688        assert_eq!(store.query("test").last_days(1).sum(), Some(1));
689    }
690
691    #[cfg(all(feature = "tokio", feature = "serde"))]
692    #[tokio::test]
693    async fn test_unified_build_with_auto_persist() {
694        use chrono::Duration;
695
696        let store = EventStoreBuilder::new()
697            .with_storage(MemoryStorage::new())
698            .auto_persist(Duration::milliseconds(50))
699            .build()
700            .unwrap();
701
702        // Should work the same - API is unified
703        store.record("test");
704        assert_eq!(store.query("test").last_days(1).sum(), Some(1));
705
706        // Wait for auto-persist
707        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
708        assert!(!store.is_dirty());
709    }
710
711    #[cfg(all(feature = "tokio", feature = "serde"))]
712    #[tokio::test]
713    async fn test_unified_build_auto_persist_no_locking_needed() {
714        use chrono::Duration;
715
716        let store = EventStoreBuilder::new()
717            .with_storage(MemoryStorage::new())
718            .auto_persist(Duration::milliseconds(50))
719            .build()
720            .unwrap();
721
722        // Record directly - no .lock() calls needed
723        store.record("event1");
724        store.record("event2");
725        store.record("event3");
726
727        // Query directly - no .lock() calls needed
728        assert_eq!(store.query("event1").last_days(1).sum(), Some(1));
729        assert_eq!(store.query("event2").last_days(1).sum(), Some(1));
730        assert_eq!(store.query("event3").last_days(1).sum(), Some(1));
731    }
732
733    // Formatter tests
734    #[cfg(feature = "serde-bincode")]
735    #[test]
736    fn test_with_format_bincode() {
737        use crate::formatter::BincodeFormat;
738
739        let builder = EventStoreBuilder::new()
740            .with_storage(MemoryStorage::new())
741            .with_format(BincodeFormat);
742
743        assert!(builder.formatter.is_some());
744    }
745
746    #[cfg(feature = "serde-json")]
747    #[test]
748    fn test_with_format_json() {
749        use crate::formatter::JsonFormat;
750
751        let builder = EventStoreBuilder::new()
752            .with_storage(MemoryStorage::new())
753            .with_format(JsonFormat);
754
755        assert!(builder.formatter.is_some());
756    }
757
758    #[cfg(feature = "serde-bincode")]
759    #[test]
760    fn test_default_formatter_is_bincode() {
761        let store = EventStoreBuilder::new()
762            .with_storage(MemoryStorage::new())
763            .build()
764            .unwrap();
765
766        // Verify store works (uses bincode by default)
767        store.record("test");
768        assert_eq!(store.query("test").last_days(1).sum(), Some(1));
769    }
770
771    #[cfg(all(feature = "serde-bincode", feature = "serde"))]
772    #[test]
773    fn test_formatter_used_in_persistence() {
774        use crate::formatter::BincodeFormat;
775
776        let store = EventStoreBuilder::new()
777            .with_storage(MemoryStorage::new())
778            .with_format(BincodeFormat)
779            .build()
780            .unwrap();
781
782        store.record("event1");
783        store.record("event2");
784
785        // Should persist successfully
786        let result = store.persist();
787        assert!(result.is_ok());
788    }
789
790    #[cfg(all(feature = "serde-json", feature = "serde"))]
791    #[test]
792    fn test_json_formatter_used_in_persistence() {
793        use crate::formatter::JsonFormat;
794
795        let store = EventStoreBuilder::new()
796            .with_storage(MemoryStorage::new())
797            .with_format(JsonFormat)
798            .build()
799            .unwrap();
800
801        store.record("event1");
802        store.record("event2");
803
804        // Should persist successfully with JSON format
805        let result = store.persist();
806        assert!(result.is_ok());
807    }
808
809    #[test]
810    #[cfg(feature = "tokio")]
811    fn test_auto_persist_negative_interval_fails() {
812        use chrono::Duration;
813
814        let result = EventStoreBuilder::new()
815            .with_storage(MemoryStorage::new())
816            .auto_persist(Duration::seconds(-60))
817            .build();
818
819        assert!(result.is_err());
820        match result {
821            Err(crate::error::Error::InvalidAutoPersistInterval(d)) => {
822                assert_eq!(d.num_seconds(), -60);
823            }
824            _ => panic!("Expected InvalidAutoPersistInterval error"),
825        }
826    }
827
828    #[test]
829    #[cfg(feature = "tokio")]
830    fn test_auto_persist_zero_interval_fails() {
831        use chrono::Duration;
832
833        let result = EventStoreBuilder::new()
834            .with_storage(MemoryStorage::new())
835            .auto_persist(Duration::zero())
836            .build();
837
838        assert!(result.is_err());
839        match result {
840            Err(crate::error::Error::InvalidAutoPersistInterval(d)) => {
841                assert!(d.num_milliseconds() <= 0);
842            }
843            _ => panic!("Expected InvalidAutoPersistInterval error"),
844        }
845    }
846}