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::environment::Environment;
11use crate::store::CacheStore;
12use crate::{MemoryStore, NamespacedStore};
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    #[cfg(feature = "cache-redis")]
71    async fn build_redis(&self) -> Result<Arc<dyn CacheStore>> {
72        let endpoint = self
73            .endpoint
74            .clone()
75            .unwrap_or_else(|| "redis://127.0.0.1:6379".to_string());
76        Ok(Arc::new(
77            crate::redis_store::RedisStore::connect(&endpoint).await?,
78        ))
79    }
80
81    #[cfg(not(feature = "cache-redis"))]
82    async fn build_redis(&self) -> Result<Arc<dyn CacheStore>> {
83        Err(doido_core::anyhow::anyhow!(
84            "cache backend 'redis' selected in config but doido-cache was built \
85             without the `cache-redis` feature"
86        ))
87    }
88
89    #[cfg(feature = "cache-memcache")]
90    async fn build_memcache(&self) -> Result<Arc<dyn CacheStore>> {
91        let endpoint = self
92            .endpoint
93            .clone()
94            .unwrap_or_else(|| "memcache://127.0.0.1:11211".to_string());
95        Ok(Arc::new(
96            crate::memcache_store::MemcacheStore::connect(endpoint).await?,
97        ))
98    }
99
100    #[cfg(not(feature = "cache-memcache"))]
101    async fn build_memcache(&self) -> Result<Arc<dyn CacheStore>> {
102        Err(doido_core::anyhow::anyhow!(
103            "cache backend 'memcache' selected in config but doido-cache was \
104             built without the `cache-memcache` feature"
105        ))
106    }
107}
108
109/// File-based config deserialized from `config/<env>.yml`. Only the `cache`
110/// section is read; other sections (server, database, logger…) are ignored.
111#[derive(Debug, Clone, Default, Deserialize)]
112pub struct YamlConfig {
113    #[serde(default)]
114    pub cache: CacheConfig,
115}
116
117impl YamlConfig {
118    /// Loads `config/<env>.yml` for the environment from [`Environment::get_env`].
119    pub fn load() -> std::io::Result<Self> {
120        Self::load_env(Environment::get_env())
121    }
122
123    /// Loads `config/<env>.yml` for a specific environment.
124    pub fn load_env(env: Environment) -> std::io::Result<Self> {
125        let path = format!("config/{}.yml", env.as_str());
126        let contents = std::fs::read_to_string(&path)?;
127        Self::from_yaml(&contents)
128    }
129
130    /// Parses a [`YamlConfig`] from a YAML string.
131    pub fn from_yaml(yaml: &str) -> std::io::Result<Self> {
132        serde_norway::from_str(yaml)
133            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
134    }
135}
136
137/// Loads the current environment's [`CacheConfig`], falling back to the default
138/// (in-memory) when the file is missing or has no `cache` section.
139pub fn load() -> CacheConfig {
140    YamlConfig::load().map(|c| c.cache).unwrap_or_default()
141}