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}