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 DummyStore;
115
116    #[async_trait::async_trait]
117    impl CacheStore for DummyStore {
118        async fn get(&self, _key: &str) -> AppResult<Option<String>> {
119            Ok(None)
120        }
121
122        async fn set(&self, _key: &str, _val: &str, _ttl: Option<Duration>) -> AppResult<()> {
123            Ok(())
124        }
125
126        async fn delete(&self, _key: &str) -> AppResult<bool> {
127            Ok(false)
128        }
129
130        async fn exists(&self, _key: &str) -> AppResult<bool> {
131            Ok(false)
132        }
133    }
134
135    #[derive(Default)]
136    struct DummyFactory;
137
138    #[async_trait::async_trait]
139    impl CacheStoreFactory for DummyFactory {
140        async fn create(&self, _config: &CacheConfig) -> AppResult<Arc<dyn CacheStore>> {
141            Ok(Arc::new(DummyStore))
142        }
143    }
144
145    #[test]
146    fn register_rejects_empty_store_name() {
147        let mut registry = CacheRegistry::new();
148        let err = registry
149            .register(" \t ", Arc::new(DummyFactory))
150            .expect_err("empty store names must be rejected");
151        assert_eq!(err.code(), ErrorCode::InvalidInput);
152    }
153
154    #[test]
155    fn register_rejects_duplicate_store_name() {
156        let mut registry = CacheRegistry::new();
157        registry
158            .register("memory", Arc::new(DummyFactory))
159            .expect("first registration should succeed");
160
161        let err = registry
162            .register("memory", Arc::new(DummyFactory))
163            .expect_err("duplicate store names must be rejected");
164        assert_eq!(err.code(), ErrorCode::AlreadyExists);
165    }
166
167    #[tokio::test]
168    async fn build_rejects_empty_selected_store() {
169        let registry = CacheRegistry::new();
170        let config = CacheConfig {
171            store: " ".into(),
172            ..CacheConfig::default()
173        };
174
175        assert_eq!(
176            registry.build(&config).await.err().map(|err| err.code()),
177            Some(ErrorCode::InvalidInput)
178        );
179    }
180
181    #[tokio::test]
182    async fn build_rejects_unregistered_selected_store() {
183        let registry = CacheRegistry::new();
184        let config = CacheConfig {
185            store: "redis".into(),
186            ..CacheConfig::default()
187        };
188
189        assert_eq!(
190            registry.build(&config).await.err().map(|err| err.code()),
191            Some(ErrorCode::NotFound)
192        );
193    }
194
195    #[tokio::test]
196    async fn build_uses_registered_factory() {
197        let mut registry = CacheRegistry::new();
198        registry
199            .register("memory", Arc::new(DummyFactory))
200            .expect("registration should succeed");
201        assert!(registry.contains("memory"));
202        assert_eq!(registry.len(), 1);
203        assert!(!registry.is_empty());
204
205        let config = CacheConfig {
206            store: " memory ".into(),
207            ..CacheConfig::default()
208        };
209
210        let store = registry
211            .build(&config)
212            .await
213            .expect("registered store should build");
214        assert!(
215            store
216                .get("missing")
217                .await
218                .expect("get should succeed")
219                .is_none()
220        );
221        store.set("k", "v", None).await.expect("set should succeed");
222        assert!(!store.delete("k").await.expect("delete should succeed"));
223        assert!(
224            !store
225                .exists("missing")
226                .await
227                .expect("exists should succeed")
228        );
229    }
230}