rainier-cache 1.0.1

A cache port for Rainier, with memory, Redis, Redis Cluster and Memcached drivers.
Documentation
//! Which cache store to build — [`CacheDriver`].

use rainier_support::setting_enum;

setting_enum! {
    /// Where cached values live.
    ///
    /// The enum names every store this crate knows how to build, whether or not
    /// the cargo feature behind it is on. That is deliberate: `CACHE_DRIVER=redis`
    /// in a build without the `redis-driver` feature is a *build* mistake, and
    /// it deserves a message saying so — not the same "unknown driver" message a
    /// typo gets.
    ///
    /// ```
    /// use rainier_cache::CacheDriver;
    /// use rainier_support::Setting;
    ///
    /// assert_eq!(CacheDriver::parse("redis-cluster").unwrap(), CacheDriver::RedisCluster);
    /// assert!(CacheDriver::parse("redys").is_err());
    /// ```
    pub enum CacheDriver: "cache driver" {
        /// This process only. Fast, and shares nothing between instances.
        ///
        /// The default because it needs no server, and the right choice for
        /// tests and single-process development. Anything cached **for
        /// correctness** rather than for speed is wrong in it — a rate-limit
        /// counter becomes `N ×` its limit across `N` instances, and a lock is
        /// not a lock.
        #[default]
        Memory = "memory",

        /// One Redis server, or a Sentinel-fronted primary.
        Redis = "redis",

        /// A sharded Redis Cluster. Keys distribute across the seed nodes.
        RedisCluster = "redis-cluster",

        /// Memcached over its text protocol.
        Memcached = "memcached",

        /// DynamoDB, with TTL doing the expiry. No server to run.
        DynamoDb = "dynamodb",
    }
}

impl CacheDriver {
    /// Whether this driver is shared between instances of the application.
    ///
    /// The question a production checklist is really asking. A rate limiter, a
    /// lock, or a cached authorisation decision needs `true` here; a memoised
    /// computation does not.
    pub fn is_shared(&self) -> bool {
        !matches!(self, Self::Memory)
    }

    /// The cargo feature that has to be on for this driver to be built.
    ///
    /// `None` for the ones that are always available.
    pub fn feature(&self) -> Option<&'static str> {
        match self {
            Self::Memory => None,
            Self::Redis => Some("redis-driver"),
            Self::RedisCluster => Some("redis-cluster"),
            Self::Memcached => Some("memcached"),
            Self::DynamoDb => Some("dynamodb"),
        }
    }
}

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

    #[test]
    fn only_memory_is_per_process() {
        assert!(!CacheDriver::Memory.is_shared());
        for driver in CacheDriver::ALL.iter().filter(|d| **d != CacheDriver::Memory) {
            assert!(driver.is_shared(), "{driver} should be shared");
        }
    }

    #[test]
    fn every_driver_but_memory_names_a_feature() {
        // The message "enable the `redis-driver` feature" is only possible if
        // the driver knows which feature that is.
        assert_eq!(CacheDriver::Memory.feature(), None);
        for driver in CacheDriver::ALL.iter().filter(|d| **d != CacheDriver::Memory) {
            assert!(driver.feature().is_some(), "{driver} should name its feature");
        }
    }

    #[test]
    fn the_wire_spellings_are_what_a_dotenv_would_hold() {
        assert_eq!(CacheDriver::RedisCluster.as_str(), "redis-cluster");
        assert_eq!(CacheDriver::DynamoDb.as_str(), "dynamodb");
    }
}