tiny-counter 0.1.0

Track event counts across time windows with fixed memory and fast queries
Documentation
// Stdlib imports
use std::result;

// Third-party imports
use chrono::Duration;
use thiserror::Error as ThisError;

// Crate imports
use crate::store::limiter::Constraint;

/// Error returned when a rate limit is exceeded.
#[derive(Debug)]
pub struct LimitExceeded {
    pub event_id: String,
    pub constraint: Constraint,
    pub retry_after: Option<Duration>,
}

/// Error types for the event-counter library.
#[derive(Debug, ThisError)]
pub enum Error {
    #[error("future events cannot be recorded")]
    FutureEvent,
    #[error("event not found: {0}")]
    EventNotFound(String),
    #[error("storage error: {0}")]
    Storage(String),
    #[error("serialization error: {0}")]
    Serialization(String),
    #[error("config mismatch: {0}")]
    ConfigMismatch(String),
    #[error("invalid hour: {0} (must be 0-23)")]
    InvalidHour(u8),
    #[error("invalid hour range: start_hour {0} >= end_hour {1}")]
    InvalidRange(u8, u8),
    #[error("invalid bucket count: {0}")]
    InvalidBucketCount(String),
    #[error("TimeUnit::Ever cannot be used in IntervalConfig (it is a query-time sentinel value)")]
    InvalidTimeUnitEver,
    #[error("auto_persist interval must be positive (got {0})")]
    InvalidAutoPersistInterval(Duration),
    #[cfg(feature = "tokio")]
    #[error("auto_persist requires storage to be configured - use with_storage() or remove auto_persist()")]
    AutoPersistRequiresStorage,
    #[error("storage requires a formatter - enable 'serde-bincode' or 'serde-json' feature, or use with_format() to provide a custom formatter")]
    NoFormatterForStorage,
    #[error("rate limit exceeded for event: {}", .0.event_id)]
    LimitExceeded(LimitExceeded),
}

/// Convenience type for Results in this library.
pub type Result<T> = result::Result<T, Error>;

impl Error {
    /// Returns a reference to the LimitExceeded details if this is a LimitExceeded error.
    pub fn as_limit_exceeded(&self) -> Option<&LimitExceeded> {
        match self {
            Error::LimitExceeded(limit_exceeded) => Some(limit_exceeded),
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_future_event_error_display() {
        let err = Error::FutureEvent;
        assert_eq!(err.to_string(), "future events cannot be recorded");
    }

    #[test]
    fn test_event_not_found_error_display() {
        let err = Error::EventNotFound("test_event".to_string());
        assert_eq!(err.to_string(), "event not found: test_event");
    }

    #[test]
    fn test_storage_error_display() {
        let err = Error::Storage("connection failed".to_string());
        assert_eq!(err.to_string(), "storage error: connection failed");
    }

    #[test]
    fn test_serialization_error_display() {
        let err = Error::Serialization("invalid JSON".to_string());
        assert_eq!(err.to_string(), "serialization error: invalid JSON");
    }

    #[test]
    fn test_config_mismatch_error_display() {
        let err = Error::ConfigMismatch("different time units".to_string());
        assert_eq!(err.to_string(), "config mismatch: different time units");
    }

    #[test]
    fn test_invalid_hour_error_display() {
        let err = Error::InvalidHour(24);
        assert_eq!(err.to_string(), "invalid hour: 24 (must be 0-23)");
    }

    #[test]
    fn test_invalid_range_error_display() {
        let err = Error::InvalidRange(17, 9);
        assert_eq!(
            err.to_string(),
            "invalid hour range: start_hour 17 >= end_hour 9"
        );
    }

    #[test]
    fn test_invalid_bucket_count_error_display() {
        let err = Error::InvalidBucketCount("bucket count must be positive".to_string());
        assert_eq!(
            err.to_string(),
            "invalid bucket count: bucket count must be positive"
        );
    }

    #[test]
    fn test_invalid_time_unit_ever_error_display() {
        let err = Error::InvalidTimeUnitEver;
        assert_eq!(
            err.to_string(),
            "TimeUnit::Ever cannot be used in IntervalConfig (it is a query-time sentinel value)"
        );
    }

    #[test]
    fn test_error_is_send_sync() {
        fn assert_send<T: Send>() {}
        fn assert_sync<T: Sync>() {}
        assert_send::<Error>();
        assert_sync::<Error>();
    }

    #[test]
    fn test_invalid_auto_persist_interval_error_display() {
        let err = Error::InvalidAutoPersistInterval(Duration::seconds(-60));
        assert_eq!(
            err.to_string(),
            "auto_persist interval must be positive (got -PT60S)"
        );
    }
}