tiny-counter 0.1.0

Track event counts across time windows with fixed memory and fast queries
Documentation
use chrono::{DateTime, Utc};

use crate::Result;

/// Clock trait for abstracting time operations.
///
/// Provides current time for the event counter system.
///
/// This trait enables time-travel testing via TestClock and production
/// use via SystemClock.
pub trait Clock: Send + Sync {
    fn now(&self) -> DateTime<Utc>;
}

/// Storage backend trait for persisting event data.
///
/// Implementations must be thread-safe (Send + Sync) and handle
/// serialization/deserialization of event data.
///
/// The `save` method takes ownership of data to avoid redundant copies.
/// Most storage backends need to own the data anyway, so this provides
/// better symmetry with `load()` and eliminates an extra allocation.
///
/// ## Transaction Support
///
/// Storage backends can optionally implement atomic multi-operation transactions
/// via `begin_transaction()`, `commit_transaction()`, and `rollback_transaction()`.
/// Backends that don't support transactions use the default no-op implementations.
///
/// When transactions are supported:
/// - `begin_transaction()` starts a new transaction
/// - All `save()` and `delete()` calls are buffered until commit
/// - `commit_transaction()` atomically applies all changes
/// - `rollback_transaction()` discards pending changes
///
/// Backends must be either in transaction mode or normal mode - mixing operations
/// across modes should return an error.
pub trait Storage: Send + Sync {
    fn save(&mut self, key: &str, data: Vec<u8>) -> Result<()>;
    fn load(&self, key: &str) -> Result<Option<Vec<u8>>>;
    fn delete(&mut self, key: &str) -> Result<()>;
    fn list_keys(&self) -> Result<Vec<String>>;

    /// Begins a transaction. All subsequent save/delete operations are buffered
    /// until commit_transaction() is called.
    ///
    /// Default implementation is a no-op for backends that don't support transactions.
    fn begin_transaction(&mut self) -> Result<()> {
        Ok(())
    }

    /// Commits the current transaction, atomically applying all buffered changes.
    ///
    /// Default implementation is a no-op for backends that don't support transactions.
    fn commit_transaction(&mut self) -> Result<()> {
        Ok(())
    }

    /// Rolls back the current transaction, discarding all buffered changes.
    ///
    /// Default implementation is a no-op for backends that don't support transactions.
    fn rollback_transaction(&mut self) -> Result<()> {
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::*;

    // Test implementation of Clock
    struct TestClockImpl {
        time: DateTime<Utc>,
    }

    impl Clock for TestClockImpl {
        fn now(&self) -> DateTime<Utc> {
            self.time
        }
    }

    // Test implementation of Storage
    struct TestStorageImpl {
        data: HashMap<String, Vec<u8>>,
    }

    impl Storage for TestStorageImpl {
        fn save(&mut self, key: &str, data: Vec<u8>) -> Result<()> {
            self.data.insert(key.to_string(), data);
            Ok(())
        }

        fn load(&self, key: &str) -> Result<Option<Vec<u8>>> {
            Ok(self.data.get(key).cloned())
        }

        fn delete(&mut self, key: &str) -> Result<()> {
            self.data.remove(key);
            Ok(())
        }

        fn list_keys(&self) -> Result<Vec<String>> {
            Ok(self.data.keys().cloned().collect())
        }
    }

    #[test]
    fn test_clock_trait_works() {
        let time = Utc::now();
        let clock = TestClockImpl { time };
        assert_eq!(clock.now(), time);
    }

    #[test]
    fn test_clock_is_send_sync() {
        fn assert_send<T: Send>() {}
        fn assert_sync<T: Sync>() {}
        assert_send::<Box<dyn Clock>>();
        assert_sync::<Box<dyn Clock>>();
    }

    #[test]
    fn test_storage_save_and_load() {
        let mut storage = TestStorageImpl {
            data: HashMap::new(),
        };

        let data = vec![1, 2, 3, 4];
        storage.save("test_key", data.clone()).unwrap();

        let loaded = storage.load("test_key").unwrap();
        assert_eq!(loaded, Some(data));
    }

    #[test]
    fn test_storage_load_nonexistent() {
        let storage = TestStorageImpl {
            data: HashMap::new(),
        };

        let loaded = storage.load("nonexistent").unwrap();
        assert_eq!(loaded, None);
    }

    #[test]
    fn test_storage_delete() {
        let mut storage = TestStorageImpl {
            data: HashMap::new(),
        };

        storage.save("test_key", vec![1, 2, 3]).unwrap();
        storage.delete("test_key").unwrap();

        let loaded = storage.load("test_key").unwrap();
        assert_eq!(loaded, None);
    }

    #[test]
    fn test_storage_is_send_sync() {
        fn assert_send<T: Send>() {}
        fn assert_sync<T: Sync>() {}
        assert_send::<Box<dyn Storage>>();
        assert_sync::<Box<dyn Storage>>();
    }
}