Skip to main content

doido_cache/
config.rs

1//! Per-environment cache configuration loaded from the `cache` section of
2//! `config/<env>.yml`.
3//!
4//! [`CacheConfig`] selects the backend (`memory`, `redis`, or `memcache`) and
5//! its `endpoint`, and [`CacheConfig::build`] turns that into a live
6//! `Arc<dyn CacheStore>`. The Redis and Memcache backends are behind the
7//! `cache-redis` / `cache-memcache` cargo features; selecting a backend whose
8//! feature is not enabled yields a clear error.
9
10use crate::store::CacheStore;
11use crate::{MemoryStore, NamespacedStore};
12use doido_core::Environment;
13use doido_core::Result;
14use serde::Deserialize;
15use std::sync::Arc;
16
17/// Which cache backend to use.
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum CacheBackend {
21    /// In-process `HashMap` + TTL. Single-process; the default.
22    #[default]
23    Memory,
24    /// Redis (`GET`/`SET EX`), distributed. Requires the `cache-redis` feature.
25    Redis,
26    /// Memcached, distributed. Requires the `cache-memcache` feature.
27    Memcache,
28}
29
30/// Cache settings, deserialized from the `cache` section of `config/<env>.yml`.
31///
32/// ```yaml
33/// cache:
34///   type: redis                       # memory | redis | memcache
35///   endpoint: redis://127.0.0.1:6379  # backend address (redis/memcache)
36///   namespace: myapp                  # optional key prefix
37/// ```
38#[derive(Debug, Clone, Default, Deserialize)]
39pub struct CacheConfig {
40    /// Backend kind. YAML key is `type`.
41    #[serde(default, rename = "type")]
42    pub backend: CacheBackend,
43    /// Connection address for the `redis`/`memcache` backends. Ignored by
44    /// `memory`. Defaults to the backend's standard localhost address.
45    #[serde(default)]
46    pub endpoint: Option<String>,
47    /// Optional key prefix applied to every key (via [`NamespacedStore`]).
48    #[serde(default)]
49    pub namespace: Option<String>,
50}
51
52impl CacheConfig {
53    /// Builds the configured [`CacheStore`], wrapping it in a [`NamespacedStore`]
54    /// when `namespace` is set. Connecting to Redis/Memcached happens here, so
55    /// this is async and can fail.
56    pub async fn build(&self) -> Result<Arc<dyn CacheStore>> {
57        let base: Arc<dyn CacheStore> = match self.backend {
58            CacheBackend::Memory => Arc::new(MemoryStore::new()),
59            CacheBackend::Redis => self.build_redis().await?,
60            CacheBackend::Memcache => self.build_memcache().await?,
61        };
62        Ok(match &self.namespace {
63            Some(prefix) if !prefix.is_empty() => {
64                Arc::new(NamespacedStore::new(base, prefix.clone()))
65            }
66            _ => base,
67        })
68    }
69}
70
71/// Multiple named cache stores (Rails' several cache stores), deserialized from
72/// a `stores` map of name → [`CacheConfig`], e.g.:
73///
74/// ```yaml
75/// stores:
76///   primary:  { type: memory }
77///   sessions: { type: redis, namespace: sess }
78/// ```
79#[derive(Debug, Clone, Default, Deserialize)]
80pub struct MultiCacheConfig {
81    #[serde(default)]
82    pub stores: std::collections::HashMap<String, CacheConfig>,
83}
84
85impl MultiCacheConfig {
86    /// Build every configured store into a [`CacheRegistry`](crate::CacheRegistry).
87    pub async fn build_registry(&self) -> Result<crate::CacheRegistry> {
88        let mut registry = crate::CacheRegistry::new();
89        for (name, config) in &self.stores {
90            registry.add(name.clone(), config.build().await?);
91        }
92        Ok(registry)
93    }
94}
95
96impl CacheConfig {
97    #[cfg(feature = "cache-redis")]
98    async fn build_redis(&self) -> Result<Arc<dyn CacheStore>> {
99        let endpoint = self
100            .endpoint
101            .clone()
102            .unwrap_or_else(|| "redis://127.0.0.1:6379".to_string());
103        Ok(Arc::new(
104            crate::redis_store::RedisStore::connect(&endpoint).await?,
105        ))
106    }
107
108    #[cfg(not(feature = "cache-redis"))]
109    async fn build_redis(&self) -> Result<Arc<dyn CacheStore>> {
110        Err(doido_core::anyhow::anyhow!(
111            "cache backend 'redis' selected in config but doido-cache was built \
112             without the `cache-redis` feature"
113        ))
114    }
115
116    #[cfg(feature = "cache-memcache")]
117    async fn build_memcache(&self) -> Result<Arc<dyn CacheStore>> {
118        let endpoint = self
119            .endpoint
120            .clone()
121            .unwrap_or_else(|| "memcache://127.0.0.1:11211".to_string());
122        Ok(Arc::new(
123            crate::memcache_store::MemcacheStore::connect(endpoint).await?,
124        ))
125    }
126
127    #[cfg(not(feature = "cache-memcache"))]
128    async fn build_memcache(&self) -> Result<Arc<dyn CacheStore>> {
129        Err(doido_core::anyhow::anyhow!(
130            "cache backend 'memcache' selected in config but doido-cache was \
131             built without the `cache-memcache` feature"
132        ))
133    }
134}
135
136/// File-based config deserialized from `config/<env>.yml`. Only the `cache`
137/// section is read; other sections (server, database, logger…) are ignored.
138#[derive(Debug, Clone, Default, Deserialize)]
139pub struct YamlConfig {
140    #[serde(default)]
141    pub cache: CacheConfig,
142}
143
144impl YamlConfig {
145    /// Loads `config/<env>.yml` for the environment from [`Environment::get_env`].
146    pub fn load() -> std::io::Result<Self> {
147        Self::load_env(Environment::get_env())
148    }
149
150    /// Loads `config/<env>.yml` for a specific environment.
151    pub fn load_env(env: Environment) -> std::io::Result<Self> {
152        let path = format!("config/{}.yml", env.as_str());
153        let contents = std::fs::read_to_string(&path)?;
154        Self::from_yaml(&contents)
155    }
156
157    /// Parses a [`YamlConfig`] from a YAML string.
158    pub fn from_yaml(yaml: &str) -> std::io::Result<Self> {
159        serde_norway::from_str(yaml)
160            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
161    }
162}
163
164/// Loads the current environment's [`CacheConfig`], falling back to the default
165/// (in-memory) when the file is missing or has no `cache` section.
166pub fn load() -> CacheConfig {
167    YamlConfig::load().map(|c| c.cache).unwrap_or_default()
168}