Skip to main content

rskit_cache/
registry.rs

1//! Explicit cache store registry.
2
3use std::collections::BTreeMap;
4use std::sync::Arc;
5use std::time::Duration;
6
7use rskit_errors::{AppError, AppResult, ErrorCode};
8
9use crate::config::CacheConfig;
10
11pub use crate::adapters::memory::register_memory;
12
13/// Minimal async cache storage operations shared by all store adapters.
14#[async_trait::async_trait]
15pub trait CacheStore: Send + Sync {
16    /// Retrieve a string value by key.
17    async fn get(&self, key: &str) -> AppResult<Option<String>>;
18    /// Store a string value with an optional TTL.
19    ///
20    /// `Duration::ZERO` is invalid. Stores should honor sub-second TTLs with at least
21    /// millisecond precision; durations below one millisecond may be rounded up.
22    async fn set(&self, key: &str, val: &str, ttl: Option<Duration>) -> AppResult<()>;
23    /// Delete a key and report whether it existed.
24    async fn delete(&self, key: &str) -> AppResult<bool>;
25    /// Check whether a key currently exists.
26    async fn exists(&self, key: &str) -> AppResult<bool>;
27}
28
29/// Factory for a named cache store adapter.
30#[async_trait::async_trait]
31pub trait CacheStoreFactory: Send + Sync {
32    /// Build a cache store from cache configuration.
33    async fn create(&self, config: &CacheConfig) -> AppResult<Arc<dyn CacheStore>>;
34}
35
36/// Explicit cache store registry.
37#[derive(Default)]
38pub struct CacheRegistry {
39    factories: BTreeMap<String, Arc<dyn CacheStoreFactory>>,
40}
41
42impl CacheRegistry {
43    /// Create an empty registry. No stores are registered implicitly.
44    #[must_use]
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Register a store factory under `name`.
50    pub fn register(
51        &mut self,
52        name: impl Into<String>,
53        factory: Arc<dyn CacheStoreFactory>,
54    ) -> AppResult<()> {
55        let name = name.into().trim().to_owned();
56        if name.is_empty() {
57            return Err(AppError::new(
58                ErrorCode::InvalidInput,
59                "cache store name is required",
60            ));
61        }
62        if self.factories.contains_key(&name) {
63            return Err(AppError::new(
64                ErrorCode::AlreadyExists,
65                format!("cache store '{name}' is already registered"),
66            ));
67        }
68        self.factories.insert(name, factory);
69        Ok(())
70    }
71
72    /// Return true when `name` is registered.
73    #[must_use]
74    pub fn contains(&self, name: &str) -> bool {
75        self.factories.contains_key(name)
76    }
77
78    /// Number of registered stores.
79    #[must_use]
80    pub fn len(&self) -> usize {
81        self.factories.len()
82    }
83
84    /// Return true when no stores are registered.
85    #[must_use]
86    pub fn is_empty(&self) -> bool {
87        self.factories.is_empty()
88    }
89
90    /// Build the store selected by [`CacheConfig::store`].
91    pub async fn build(&self, config: &CacheConfig) -> AppResult<Arc<dyn CacheStore>> {
92        let store = config.store.trim();
93        if store.is_empty() {
94            return Err(AppError::new(
95                ErrorCode::InvalidInput,
96                "cache store name is required",
97            ));
98        }
99        let factory = self.factories.get(store).ok_or_else(|| {
100            AppError::new(
101                ErrorCode::NotFound,
102                format!("cache store '{store}' is not registered"),
103            )
104        })?;
105        factory.create(config).await
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[derive(Default)]
114    struct DummyFactory;
115
116    #[async_trait::async_trait]
117    impl CacheStoreFactory for DummyFactory {
118        async fn create(&self, _config: &CacheConfig) -> AppResult<Arc<dyn CacheStore>> {
119            Err(AppError::new(ErrorCode::Internal, "not used"))
120        }
121    }
122
123    #[test]
124    fn register_rejects_empty_store_name() {
125        let mut registry = CacheRegistry::new();
126        let err = registry
127            .register(" \t ", Arc::new(DummyFactory))
128            .expect_err("empty store names must be rejected");
129        assert_eq!(err.code(), ErrorCode::InvalidInput);
130    }
131
132    #[test]
133    fn register_rejects_duplicate_store_name() {
134        let mut registry = CacheRegistry::new();
135        registry
136            .register("memory", Arc::new(DummyFactory))
137            .expect("first registration should succeed");
138
139        let err = registry
140            .register("memory", Arc::new(DummyFactory))
141            .expect_err("duplicate store names must be rejected");
142        assert_eq!(err.code(), ErrorCode::AlreadyExists);
143    }
144
145    #[tokio::test]
146    async fn build_rejects_empty_selected_store() {
147        let registry = CacheRegistry::new();
148        let config = CacheConfig {
149            store: " ".into(),
150            ..CacheConfig::default()
151        };
152
153        let Err(err) = registry.build(&config).await else {
154            panic!("empty selected stores must be rejected");
155        };
156        assert_eq!(err.code(), ErrorCode::InvalidInput);
157    }
158
159    #[tokio::test]
160    async fn build_rejects_unregistered_selected_store() {
161        let registry = CacheRegistry::new();
162        let config = CacheConfig {
163            store: "redis".into(),
164            ..CacheConfig::default()
165        };
166
167        let Err(err) = registry.build(&config).await else {
168            panic!("unregistered stores must be rejected");
169        };
170        assert_eq!(err.code(), ErrorCode::NotFound);
171    }
172}