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