Skip to main content

rainier_cache/
driver.rs

1//! Which cache store to build — [`CacheDriver`].
2
3use rainier_support::setting_enum;
4
5setting_enum! {
6    /// Where cached values live.
7    ///
8    /// The enum names every store this crate knows how to build, whether or not
9    /// the cargo feature behind it is on. That is deliberate: `CACHE_DRIVER=redis`
10    /// in a build without the `redis-driver` feature is a *build* mistake, and
11    /// it deserves a message saying so — not the same "unknown driver" message a
12    /// typo gets.
13    ///
14    /// ```
15    /// use rainier_cache::CacheDriver;
16    /// use rainier_support::Setting;
17    ///
18    /// assert_eq!(CacheDriver::parse("redis-cluster").unwrap(), CacheDriver::RedisCluster);
19    /// assert!(CacheDriver::parse("redys").is_err());
20    /// ```
21    pub enum CacheDriver: "cache driver" {
22        /// This process only. Fast, and shares nothing between instances.
23        ///
24        /// The default because it needs no server, and the right choice for
25        /// tests and single-process development. Anything cached **for
26        /// correctness** rather than for speed is wrong in it — a rate-limit
27        /// counter becomes `N ×` its limit across `N` instances, and a lock is
28        /// not a lock.
29        #[default]
30        Memory = "memory",
31
32        /// One Redis server, or a Sentinel-fronted primary.
33        Redis = "redis",
34
35        /// A sharded Redis Cluster. Keys distribute across the seed nodes.
36        RedisCluster = "redis-cluster",
37
38        /// Memcached over its text protocol.
39        Memcached = "memcached",
40
41        /// DynamoDB, with TTL doing the expiry. No server to run.
42        DynamoDb = "dynamodb",
43    }
44}
45
46impl CacheDriver {
47    /// Whether this driver is shared between instances of the application.
48    ///
49    /// The question a production checklist is really asking. A rate limiter, a
50    /// lock, or a cached authorisation decision needs `true` here; a memoised
51    /// computation does not.
52    pub fn is_shared(&self) -> bool {
53        !matches!(self, Self::Memory)
54    }
55
56    /// The cargo feature that has to be on for this driver to be built.
57    ///
58    /// `None` for the ones that are always available.
59    pub fn feature(&self) -> Option<&'static str> {
60        match self {
61            Self::Memory => None,
62            Self::Redis => Some("redis-driver"),
63            Self::RedisCluster => Some("redis-cluster"),
64            Self::Memcached => Some("memcached"),
65            Self::DynamoDb => Some("dynamodb"),
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use rainier_support::Setting;
74
75    #[test]
76    fn only_memory_is_per_process() {
77        assert!(!CacheDriver::Memory.is_shared());
78        for driver in CacheDriver::ALL.iter().filter(|d| **d != CacheDriver::Memory) {
79            assert!(driver.is_shared(), "{driver} should be shared");
80        }
81    }
82
83    #[test]
84    fn every_driver_but_memory_names_a_feature() {
85        // The message "enable the `redis-driver` feature" is only possible if
86        // the driver knows which feature that is.
87        assert_eq!(CacheDriver::Memory.feature(), None);
88        for driver in CacheDriver::ALL.iter().filter(|d| **d != CacheDriver::Memory) {
89            assert!(driver.feature().is_some(), "{driver} should name its feature");
90        }
91    }
92
93    #[test]
94    fn the_wire_spellings_are_what_a_dotenv_would_hold() {
95        assert_eq!(CacheDriver::RedisCluster.as_str(), "redis-cluster");
96        assert_eq!(CacheDriver::DynamoDb.as_str(), "dynamodb");
97    }
98}