1use crate::error::StorageError;
9use async_trait::async_trait;
10use chrono::{DateTime, Utc};
11use razeri_plugin_contract::StoragePlatform;
12use std::collections::HashMap;
13use std::sync::Arc;
14
15#[derive(Debug, Clone)]
17pub struct StorageListEntry {
18 pub name: String,
20 pub size_bytes: u64,
22 pub is_directory: bool,
24}
25
26#[derive(Debug, Clone)]
28pub struct StorageMetadata {
29 pub size_bytes: u64,
31 pub modified: Option<DateTime<Utc>>,
33 pub content_type: Option<String>,
35 pub checksum: Option<String>,
37}
38
39#[async_trait]
44pub trait StorageProviderFactory: Send + Sync {
45 fn name(&self) -> &str;
47
48 fn display_name(&self) -> &str;
50
51 fn known_platform(&self) -> Option<StoragePlatform>;
53
54 async fn create(
56 &self,
57 config: &HashMap<String, String>,
58 ) -> Result<Box<dyn StorageOps>, StorageError>;
59
60 fn validate_config(&self, config: &HashMap<String, String>) -> Vec<String>;
62
63 fn required_config_keys(&self) -> Vec<&str>;
65}
66
67#[async_trait]
72pub trait StorageOps: Send + Sync {
73 async fn list(&self, path: &str) -> Result<Vec<StorageListEntry>, StorageError>;
75
76 async fn exists(&self, path: &str) -> Result<bool, StorageError>;
78
79 async fn read(&self, path: &str) -> Result<Vec<u8>, StorageError>;
81
82 async fn write(&self, path: &str, content: &[u8]) -> Result<(), StorageError>;
84
85 async fn metadata(&self, path: &str) -> Result<StorageMetadata, StorageError>;
87
88 async fn move_file(&self, src: &str, dst: &str) -> Result<(), StorageError>;
90
91 async fn delete(&self, path: &str) -> Result<(), StorageError>;
93
94 async fn mkdir(&self, path: &str) -> Result<(), StorageError>;
96}
97
98pub struct StorageRegistry {
100 known: HashMap<StoragePlatform, Arc<dyn StorageProviderFactory>>,
101 extensions: HashMap<String, Arc<dyn StorageProviderFactory>>,
102}
103
104impl StorageRegistry {
105 #[must_use]
107 pub fn new() -> Self {
108 Self { known: HashMap::new(), extensions: HashMap::new() }
109 }
110
111 pub fn register_known(
113 &mut self,
114 platform: StoragePlatform,
115 factory: Arc<dyn StorageProviderFactory>,
116 ) {
117 let name = factory.name().to_string();
118 self.known.insert(platform, Arc::clone(&factory));
119 self.extensions.insert(name, factory);
120 }
121
122 pub fn register_extension(&mut self, name: String, factory: Arc<dyn StorageProviderFactory>) {
124 self.extensions.insert(name, factory);
125 }
126
127 #[must_use]
129 pub fn get(&self, platform: &StoragePlatform) -> Option<&Arc<dyn StorageProviderFactory>> {
130 self.known.get(platform)
131 }
132
133 #[must_use]
135 pub fn get_by_name(&self, name: &str) -> Option<&Arc<dyn StorageProviderFactory>> {
136 self.extensions.get(name)
137 }
138
139 #[must_use]
141 pub fn resolve(&self, target: &str) -> Option<Arc<dyn StorageProviderFactory>> {
142 self.extensions.get(target).cloned()
143 }
144
145 #[must_use]
147 pub fn list(&self) -> Vec<(&str, &str)> {
148 self.extensions.values().map(|f| (f.name(), f.display_name())).collect()
149 }
150}
151
152impl Default for StorageRegistry {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 struct MockStorageOps;
163
164 #[async_trait]
165 impl StorageOps for MockStorageOps {
166 async fn list(&self, _path: &str) -> Result<Vec<StorageListEntry>, StorageError> {
167 Ok(vec![StorageListEntry {
168 name: "file1.csv".to_string(),
169 size_bytes: 1024,
170 is_directory: false,
171 }])
172 }
173
174 async fn exists(&self, _path: &str) -> Result<bool, StorageError> {
175 Ok(true)
176 }
177
178 async fn read(&self, _path: &str) -> Result<Vec<u8>, StorageError> {
179 Ok(b"test data".to_vec())
180 }
181
182 async fn write(&self, _path: &str, _content: &[u8]) -> Result<(), StorageError> {
183 Ok(())
184 }
185
186 async fn metadata(&self, _path: &str) -> Result<StorageMetadata, StorageError> {
187 Ok(StorageMetadata {
188 size_bytes: 42,
189 modified: None,
190 content_type: None,
191 checksum: None,
192 })
193 }
194
195 async fn move_file(&self, _src: &str, _dst: &str) -> Result<(), StorageError> {
196 Ok(())
197 }
198
199 async fn delete(&self, _path: &str) -> Result<(), StorageError> {
200 Ok(())
201 }
202
203 async fn mkdir(&self, _path: &str) -> Result<(), StorageError> {
204 Ok(())
205 }
206 }
207
208 struct MockStorageFactory {
209 name: &'static str,
210 display_name: &'static str,
211 known_platform: Option<StoragePlatform>,
212 }
213
214 impl MockStorageFactory {
215 fn s3() -> Self {
216 Self {
217 name: "s3",
218 display_name: "Amazon S3",
219 known_platform: Some(StoragePlatform::S3),
220 }
221 }
222
223 fn kafka() -> Self {
224 Self { name: "kafka", display_name: "Apache Kafka", known_platform: None }
225 }
226 }
227
228 #[async_trait]
229 impl StorageProviderFactory for MockStorageFactory {
230 fn name(&self) -> &str {
231 self.name
232 }
233
234 fn display_name(&self) -> &str {
235 self.display_name
236 }
237
238 fn known_platform(&self) -> Option<StoragePlatform> {
239 self.known_platform
240 }
241
242 async fn create(
243 &self,
244 _config: &HashMap<String, String>,
245 ) -> Result<Box<dyn StorageOps>, StorageError> {
246 Ok(Box::new(MockStorageOps))
247 }
248
249 fn validate_config(&self, config: &HashMap<String, String>) -> Vec<String> {
250 let mut errors = Vec::new();
251 for key in self.required_config_keys() {
252 if !config.contains_key(key) {
253 errors.push(format!("missing required key: {key}"));
254 }
255 }
256 errors
257 }
258
259 fn required_config_keys(&self) -> Vec<&str> {
260 vec!["bucket", "region"]
261 }
262 }
263
264 #[tokio::test]
265 async fn storage_factory_creates_operations() {
266 let factory = MockStorageFactory::s3();
267 let config = HashMap::from([
268 ("bucket".to_string(), "test-bucket".to_string()),
269 ("region".to_string(), "us-east-1".to_string()),
270 ]);
271 let ops = factory.create(&config).await.unwrap();
272 assert!(ops.exists("test").await.unwrap());
273 }
274
275 #[test]
276 fn storage_registry_dual_key_lookup() {
277 let mut registry = StorageRegistry::new();
278 registry.register_known(StoragePlatform::S3, Arc::new(MockStorageFactory::s3()));
279
280 assert!(registry.get(&StoragePlatform::S3).is_some());
281 assert!(registry.get_by_name("s3").is_some());
282 }
283
284 #[test]
285 fn storage_config_validation_catches_missing_keys() {
286 let factory = MockStorageFactory::s3();
287 let config = HashMap::new();
288 let errors = factory.validate_config(&config);
289 assert_eq!(errors.len(), 2);
290 assert!(errors[0].contains("bucket"));
291 }
292
293 #[test]
294 fn storage_extension_registration() {
295 let mut registry = StorageRegistry::new();
296 registry.register_extension("kafka".to_string(), Arc::new(MockStorageFactory::kafka()));
297 assert!(registry.get_by_name("kafka").is_some());
298 }
299
300 #[test]
301 fn storage_registry_list_shows_all_providers() {
302 let mut registry = StorageRegistry::new();
303 registry.register_known(StoragePlatform::S3, Arc::new(MockStorageFactory::s3()));
304 registry.register_extension("kafka".to_string(), Arc::new(MockStorageFactory::kafka()));
305 assert_eq!(registry.list().len(), 2);
306 }
307
308 #[test]
309 fn storage_registry_resolve_covers_both_known_and_extension() {
310 let mut registry = StorageRegistry::new();
311 registry.register_known(StoragePlatform::S3, Arc::new(MockStorageFactory::s3()));
312 registry.register_extension("kafka".to_string(), Arc::new(MockStorageFactory::kafka()));
313
314 assert!(registry.resolve("s3").is_some());
315 assert!(registry.resolve("kafka").is_some());
316 assert!(registry.resolve("nonexistent").is_none());
317 }
318}