Skip to main content

rskit_storage/store/
registry.rs

1//! Explicit storage backend registry.
2
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use rskit_errors::{AppError, AppResult, ErrorCode};
7use serde::{Deserialize, Serialize};
8
9use super::{FileStore, LocalStore, LocalStoreConfig};
10
11/// Config-driven storage backend selection.
12#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct StorageConfig {
14    /// Backend name looked up in an injected [`StorageRegistry`].
15    #[serde(default = "default_backend")]
16    pub backend: String,
17    /// Local filesystem backend options.
18    #[serde(default)]
19    pub local: LocalStoreConfig,
20}
21
22impl Default for StorageConfig {
23    fn default() -> Self {
24        Self {
25            backend: default_backend(),
26            local: LocalStoreConfig::default(),
27        }
28    }
29}
30
31fn default_backend() -> String {
32    "local".to_owned()
33}
34
35/// Async factory for storage backends.
36#[async_trait::async_trait]
37pub trait StorageFactory: Send + Sync {
38    /// Build a storage backend from config.
39    async fn create(&self, config: &StorageConfig) -> AppResult<Arc<dyn FileStore>>;
40}
41
42/// Explicit storage backend registry.
43#[derive(Default)]
44pub struct StorageRegistry {
45    factories: BTreeMap<String, Arc<dyn StorageFactory>>,
46}
47
48impl StorageRegistry {
49    /// Create an empty registry. No backend is registered implicitly.
50    #[must_use]
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Register a backend factory under `name`.
56    pub fn register(
57        &mut self,
58        name: impl Into<String>,
59        factory: Arc<dyn StorageFactory>,
60    ) -> AppResult<()> {
61        let name = name.into().trim().to_owned();
62        if name.is_empty() {
63            return Err(AppError::new(
64                ErrorCode::InvalidInput,
65                "storage backend name is required",
66            ));
67        }
68        if self.factories.contains_key(&name) {
69            return Err(AppError::new(
70                ErrorCode::AlreadyExists,
71                format!("storage backend '{name}' is already registered"),
72            ));
73        }
74        self.factories.insert(name, factory);
75        Ok(())
76    }
77
78    /// Return true when a backend exists in the registry.
79    #[must_use]
80    pub fn contains(&self, name: &str) -> bool {
81        self.factories.contains_key(name)
82    }
83
84    /// Number of registered backend factories.
85    #[must_use]
86    pub fn len(&self) -> usize {
87        self.factories.len()
88    }
89
90    /// Return true when no backends are registered.
91    #[must_use]
92    pub fn is_empty(&self) -> bool {
93        self.factories.is_empty()
94    }
95
96    /// Build the backend selected by [`StorageConfig::backend`].
97    pub async fn build(&self, config: &StorageConfig) -> AppResult<Arc<dyn FileStore>> {
98        let backend = config.backend.trim();
99        if backend.is_empty() {
100            return Err(AppError::new(
101                ErrorCode::InvalidInput,
102                "storage backend name is required",
103            ));
104        }
105        self.factories
106            .get(backend)
107            .ok_or_else(|| {
108                AppError::new(
109                    ErrorCode::NotFound,
110                    format!("storage backend '{backend}' is not registered"),
111                )
112            })?
113            .create(config)
114            .await
115    }
116}
117
118struct LocalFactory;
119
120#[async_trait::async_trait]
121impl StorageFactory for LocalFactory {
122    async fn create(&self, config: &StorageConfig) -> AppResult<Arc<dyn FileStore>> {
123        Ok(Arc::new(LocalStore::new(config.local.clone())?))
124    }
125}
126
127/// Explicitly register the local filesystem backend.
128pub fn register_local(registry: &mut StorageRegistry) -> AppResult<()> {
129    registry.register("local", Arc::new(LocalFactory))
130}
131
132#[cfg(test)]
133mod tests {
134    use std::sync::atomic::{AtomicUsize, Ordering};
135
136    use bytes::Bytes;
137
138    use super::*;
139    use crate::FileSource;
140    use crate::store::UploadOptions;
141
142    struct CountingFactory {
143        calls: Arc<AtomicUsize>,
144    }
145
146    #[async_trait::async_trait]
147    impl StorageFactory for CountingFactory {
148        async fn create(&self, _config: &StorageConfig) -> AppResult<Arc<dyn FileStore>> {
149            self.calls.fetch_add(1, Ordering::SeqCst);
150            Ok(Arc::new(LocalStore::new(LocalStoreConfig::default())?))
151        }
152    }
153
154    #[tokio::test]
155    async fn registry_registers_and_builds_explicit_factories() {
156        let calls = Arc::new(AtomicUsize::new(0));
157        let mut registry = StorageRegistry::new();
158        assert!(registry.is_empty());
159        assert_eq!(registry.len(), 0);
160
161        registry
162            .register(
163                " memory ",
164                Arc::new(CountingFactory {
165                    calls: Arc::clone(&calls),
166                }),
167            )
168            .unwrap();
169
170        assert!(registry.contains("memory"));
171        assert_eq!(registry.len(), 1);
172        let store = registry
173            .build(&StorageConfig {
174                backend: "memory".to_string(),
175                local: LocalStoreConfig::default(),
176            })
177            .await
178            .unwrap();
179        store
180            .upload(
181                &FileSource::from_bytes(Bytes::from_static(b"data")),
182                "item.bin",
183                UploadOptions::new(),
184            )
185            .await
186            .unwrap();
187        assert_eq!(calls.load(Ordering::SeqCst), 1);
188    }
189
190    #[tokio::test]
191    async fn registry_rejects_empty_duplicate_and_missing_backends() {
192        let mut registry = StorageRegistry::new();
193        let factory = Arc::new(CountingFactory {
194            calls: Arc::new(AtomicUsize::new(0)),
195        });
196
197        assert_eq!(
198            registry.register(" ", factory.clone()).unwrap_err().code(),
199            ErrorCode::InvalidInput
200        );
201        registry.register("local", factory.clone()).unwrap();
202        assert_eq!(
203            registry.register("local", factory).unwrap_err().code(),
204            ErrorCode::AlreadyExists
205        );
206        assert_eq!(
207            registry
208                .build(&StorageConfig {
209                    backend: " ".to_string(),
210                    local: LocalStoreConfig::default(),
211                })
212                .await
213                .err()
214                .unwrap()
215                .code(),
216            ErrorCode::InvalidInput
217        );
218        assert_eq!(
219            registry
220                .build(&StorageConfig {
221                    backend: "missing".to_string(),
222                    local: LocalStoreConfig::default(),
223                })
224                .await
225                .err()
226                .unwrap()
227                .code(),
228            ErrorCode::NotFound
229        );
230    }
231
232    #[tokio::test]
233    async fn local_registration_builds_local_store_from_config() {
234        let dir = tempfile::tempdir().unwrap();
235        let mut registry = StorageRegistry::new();
236        register_local(&mut registry).unwrap();
237
238        let store = registry
239            .build(&StorageConfig {
240                backend: "local".to_string(),
241                local: LocalStoreConfig {
242                    root_dir: dir.path().to_path_buf(),
243                    auto_create: false,
244                },
245            })
246            .await
247            .unwrap();
248
249        store
250            .upload(
251                &FileSource::from_bytes(Bytes::from_static(b"local")),
252                "local.bin",
253                UploadOptions::new(),
254            )
255            .await
256            .unwrap();
257        assert!(dir.path().join("local.bin").exists());
258    }
259}