rskit_storage/store/
registry.rs1use 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#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct StorageConfig {
14 #[serde(default = "default_backend")]
16 pub backend: String,
17 #[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_trait::async_trait]
37pub trait StorageFactory: Send + Sync {
38 async fn create(&self, config: &StorageConfig) -> AppResult<Arc<dyn FileStore>>;
40}
41
42#[derive(Default)]
44pub struct StorageRegistry {
45 factories: BTreeMap<String, Arc<dyn StorageFactory>>,
46}
47
48impl StorageRegistry {
49 #[must_use]
51 pub fn new() -> Self {
52 Self::default()
53 }
54
55 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 #[must_use]
80 pub fn contains(&self, name: &str) -> bool {
81 self.factories.contains_key(name)
82 }
83
84 #[must_use]
86 pub fn len(&self) -> usize {
87 self.factories.len()
88 }
89
90 #[must_use]
92 pub fn is_empty(&self) -> bool {
93 self.factories.is_empty()
94 }
95
96 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
127pub 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}