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.
21    /// Stores should honor sub-second TTLs with at least millisecond precision;
22    /// durations below one millisecond may be rounded up.
23    async fn set(&self, key: &str, val: &str, ttl: Option<Duration>) -> AppResult<()>;
24    /// Delete a key and report whether it existed.
25    async fn delete(&self, key: &str) -> AppResult<bool>;
26    /// Check whether a key currently exists.
27    async fn exists(&self, key: &str) -> AppResult<bool>;
28}
29
30/// Factory for a named cache store adapter.
31#[async_trait::async_trait]
32pub trait CacheStoreFactory: Send + Sync {
33    /// Build a cache store from cache configuration.
34    async fn create(&self, config: &CacheConfig) -> AppResult<Arc<dyn CacheStore>>;
35}
36
37/// Explicit cache store registry.
38#[derive(Default)]
39pub struct CacheRegistry {
40    factories: BTreeMap<String, Arc<dyn CacheStoreFactory>>,
41}
42
43impl CacheRegistry {
44    /// Create an empty registry. No stores are registered implicitly.
45    #[must_use]
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Register a store factory under `name`.
51    pub fn register(
52        &mut self,
53        name: impl Into<String>,
54        factory: Arc<dyn CacheStoreFactory>,
55    ) -> AppResult<()> {
56        let name = name.into().trim().to_owned();
57        if name.is_empty() {
58            return Err(AppError::new(
59                ErrorCode::InvalidInput,
60                "cache store name is required",
61            ));
62        }
63        if self.factories.contains_key(&name) {
64            return Err(AppError::new(
65                ErrorCode::AlreadyExists,
66                format!("cache store '{name}' is already registered"),
67            ));
68        }
69        self.factories.insert(name, factory);
70        Ok(())
71    }
72
73    /// Return true when `name` is registered.
74    #[must_use]
75    pub fn contains(&self, name: &str) -> bool {
76        self.factories.contains_key(name)
77    }
78
79    /// Number of registered stores.
80    #[must_use]
81    pub fn len(&self) -> usize {
82        self.factories.len()
83    }
84
85    /// Return true when no stores are registered.
86    #[must_use]
87    pub fn is_empty(&self) -> bool {
88        self.factories.is_empty()
89    }
90
91    /// Build the store selected by [`CacheConfig::store`].
92    pub async fn build(&self, config: &CacheConfig) -> AppResult<Arc<dyn CacheStore>> {
93        let store = config.store.trim();
94        if store.is_empty() {
95            return Err(AppError::new(
96                ErrorCode::InvalidInput,
97                "cache store name is required",
98            ));
99        }
100        let factory = self.factories.get(store).ok_or_else(|| {
101            AppError::new(
102                ErrorCode::NotFound,
103                format!("cache store '{store}' is not registered"),
104            )
105        })?;
106        factory.create(config).await
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[derive(Default)]
115    struct DummyStore;
116
117    #[async_trait::async_trait]
118    impl CacheStore for DummyStore {
119        async fn get(&self, _key: &str) -> AppResult<Option<String>> {
120            Ok(None)
121        }
122
123        async fn set(&self, _key: &str, _val: &str, _ttl: Option<Duration>) -> AppResult<()> {
124            Ok(())
125        }
126
127        async fn delete(&self, _key: &str) -> AppResult<bool> {
128            Ok(false)
129        }
130
131        async fn exists(&self, _key: &str) -> AppResult<bool> {
132            Ok(false)
133        }
134    }
135
136    #[derive(Default)]
137    struct DummyFactory;
138
139    #[async_trait::async_trait]
140    impl CacheStoreFactory for DummyFactory {
141        async fn create(&self, _config: &CacheConfig) -> AppResult<Arc<dyn CacheStore>> {
142            Ok(Arc::new(DummyStore))
143        }
144    }
145
146    #[test]
147    fn register_rejects_empty_store_name() {
148        let mut registry = CacheRegistry::new();
149        let err = registry
150            .register(" \t ", Arc::new(DummyFactory))
151            .expect_err("empty store names must be rejected");
152        assert_eq!(err.code(), ErrorCode::InvalidInput);
153    }
154
155    #[test]
156    fn register_rejects_duplicate_store_name() {
157        let mut registry = CacheRegistry::new();
158        registry
159            .register("memory", Arc::new(DummyFactory))
160            .expect("first registration should succeed");
161
162        let err = registry
163            .register("memory", Arc::new(DummyFactory))
164            .expect_err("duplicate store names must be rejected");
165        assert_eq!(err.code(), ErrorCode::AlreadyExists);
166    }
167
168    #[tokio::test]
169    async fn build_rejects_empty_selected_store() {
170        let registry = CacheRegistry::new();
171        let config = CacheConfig {
172            store: " ".into(),
173            ..CacheConfig::default()
174        };
175
176        assert_eq!(
177            registry.build(&config).await.err().map(|err| err.code()),
178            Some(ErrorCode::InvalidInput)
179        );
180    }
181
182    #[tokio::test]
183    async fn build_rejects_unregistered_selected_store() {
184        let registry = CacheRegistry::new();
185        let config = CacheConfig {
186            store: "redis".into(),
187            ..CacheConfig::default()
188        };
189
190        assert_eq!(
191            registry.build(&config).await.err().map(|err| err.code()),
192            Some(ErrorCode::NotFound)
193        );
194    }
195
196    #[tokio::test]
197    async fn build_uses_registered_factory() {
198        let mut registry = CacheRegistry::new();
199        registry
200            .register("memory", Arc::new(DummyFactory))
201            .expect("registration should succeed");
202        assert!(registry.contains("memory"));
203        assert_eq!(registry.len(), 1);
204        assert!(!registry.is_empty());
205
206        let config = CacheConfig {
207            store: " memory ".into(),
208            ..CacheConfig::default()
209        };
210
211        let store = registry
212            .build(&config)
213            .await
214            .expect("registered store should build");
215        assert!(
216            store
217                .get("missing")
218                .await
219                .expect("get should succeed")
220                .is_none()
221        );
222        store.set("k", "v", None).await.expect("set should succeed");
223        assert!(!store.delete("k").await.expect("delete should succeed"));
224        assert!(
225            !store
226                .exists("missing")
227                .await
228                .expect("exists should succeed")
229        );
230    }
231}