use crate::error::StorageError;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use razeri_plugin_contract::StoragePlatform;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct StorageListEntry {
pub name: String,
pub size_bytes: u64,
pub is_directory: bool,
}
#[derive(Debug, Clone)]
pub struct StorageMetadata {
pub size_bytes: u64,
pub modified: Option<DateTime<Utc>>,
pub content_type: Option<String>,
pub checksum: Option<String>,
}
#[async_trait]
pub trait StorageProviderFactory: Send + Sync {
fn name(&self) -> &str;
fn display_name(&self) -> &str;
fn known_platform(&self) -> Option<StoragePlatform>;
async fn create(
&self,
config: &HashMap<String, String>,
) -> Result<Box<dyn StorageOps>, StorageError>;
fn validate_config(&self, config: &HashMap<String, String>) -> Vec<String>;
fn required_config_keys(&self) -> Vec<&str>;
}
#[async_trait]
pub trait StorageOps: Send + Sync {
async fn list(&self, path: &str) -> Result<Vec<StorageListEntry>, StorageError>;
async fn exists(&self, path: &str) -> Result<bool, StorageError>;
async fn read(&self, path: &str) -> Result<Vec<u8>, StorageError>;
async fn write(&self, path: &str, content: &[u8]) -> Result<(), StorageError>;
async fn metadata(&self, path: &str) -> Result<StorageMetadata, StorageError>;
async fn move_file(&self, src: &str, dst: &str) -> Result<(), StorageError>;
async fn delete(&self, path: &str) -> Result<(), StorageError>;
async fn mkdir(&self, path: &str) -> Result<(), StorageError>;
}
pub struct StorageRegistry {
known: HashMap<StoragePlatform, Arc<dyn StorageProviderFactory>>,
extensions: HashMap<String, Arc<dyn StorageProviderFactory>>,
}
impl StorageRegistry {
#[must_use]
pub fn new() -> Self {
Self { known: HashMap::new(), extensions: HashMap::new() }
}
pub fn register_known(
&mut self,
platform: StoragePlatform,
factory: Arc<dyn StorageProviderFactory>,
) {
let name = factory.name().to_string();
self.known.insert(platform, Arc::clone(&factory));
self.extensions.insert(name, factory);
}
pub fn register_extension(&mut self, name: String, factory: Arc<dyn StorageProviderFactory>) {
self.extensions.insert(name, factory);
}
#[must_use]
pub fn get(&self, platform: &StoragePlatform) -> Option<&Arc<dyn StorageProviderFactory>> {
self.known.get(platform)
}
#[must_use]
pub fn get_by_name(&self, name: &str) -> Option<&Arc<dyn StorageProviderFactory>> {
self.extensions.get(name)
}
#[must_use]
pub fn resolve(&self, target: &str) -> Option<Arc<dyn StorageProviderFactory>> {
self.extensions.get(target).cloned()
}
#[must_use]
pub fn list(&self) -> Vec<(&str, &str)> {
self.extensions.values().map(|f| (f.name(), f.display_name())).collect()
}
}
impl Default for StorageRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockStorageOps;
#[async_trait]
impl StorageOps for MockStorageOps {
async fn list(&self, _path: &str) -> Result<Vec<StorageListEntry>, StorageError> {
Ok(vec![StorageListEntry {
name: "file1.csv".to_string(),
size_bytes: 1024,
is_directory: false,
}])
}
async fn exists(&self, _path: &str) -> Result<bool, StorageError> {
Ok(true)
}
async fn read(&self, _path: &str) -> Result<Vec<u8>, StorageError> {
Ok(b"test data".to_vec())
}
async fn write(&self, _path: &str, _content: &[u8]) -> Result<(), StorageError> {
Ok(())
}
async fn metadata(&self, _path: &str) -> Result<StorageMetadata, StorageError> {
Ok(StorageMetadata {
size_bytes: 42,
modified: None,
content_type: None,
checksum: None,
})
}
async fn move_file(&self, _src: &str, _dst: &str) -> Result<(), StorageError> {
Ok(())
}
async fn delete(&self, _path: &str) -> Result<(), StorageError> {
Ok(())
}
async fn mkdir(&self, _path: &str) -> Result<(), StorageError> {
Ok(())
}
}
struct MockStorageFactory {
name: &'static str,
display_name: &'static str,
known_platform: Option<StoragePlatform>,
}
impl MockStorageFactory {
fn s3() -> Self {
Self {
name: "s3",
display_name: "Amazon S3",
known_platform: Some(StoragePlatform::S3),
}
}
fn kafka() -> Self {
Self { name: "kafka", display_name: "Apache Kafka", known_platform: None }
}
}
#[async_trait]
impl StorageProviderFactory for MockStorageFactory {
fn name(&self) -> &str {
self.name
}
fn display_name(&self) -> &str {
self.display_name
}
fn known_platform(&self) -> Option<StoragePlatform> {
self.known_platform
}
async fn create(
&self,
_config: &HashMap<String, String>,
) -> Result<Box<dyn StorageOps>, StorageError> {
Ok(Box::new(MockStorageOps))
}
fn validate_config(&self, config: &HashMap<String, String>) -> Vec<String> {
let mut errors = Vec::new();
for key in self.required_config_keys() {
if !config.contains_key(key) {
errors.push(format!("missing required key: {key}"));
}
}
errors
}
fn required_config_keys(&self) -> Vec<&str> {
vec!["bucket", "region"]
}
}
#[tokio::test]
async fn storage_factory_creates_operations() {
let factory = MockStorageFactory::s3();
let config = HashMap::from([
("bucket".to_string(), "test-bucket".to_string()),
("region".to_string(), "us-east-1".to_string()),
]);
let ops = factory.create(&config).await.unwrap();
assert!(ops.exists("test").await.unwrap());
}
#[test]
fn storage_registry_dual_key_lookup() {
let mut registry = StorageRegistry::new();
registry.register_known(StoragePlatform::S3, Arc::new(MockStorageFactory::s3()));
assert!(registry.get(&StoragePlatform::S3).is_some());
assert!(registry.get_by_name("s3").is_some());
}
#[test]
fn storage_config_validation_catches_missing_keys() {
let factory = MockStorageFactory::s3();
let config = HashMap::new();
let errors = factory.validate_config(&config);
assert_eq!(errors.len(), 2);
assert!(errors[0].contains("bucket"));
}
#[test]
fn storage_extension_registration() {
let mut registry = StorageRegistry::new();
registry.register_extension("kafka".to_string(), Arc::new(MockStorageFactory::kafka()));
assert!(registry.get_by_name("kafka").is_some());
}
#[test]
fn storage_registry_list_shows_all_providers() {
let mut registry = StorageRegistry::new();
registry.register_known(StoragePlatform::S3, Arc::new(MockStorageFactory::s3()));
registry.register_extension("kafka".to_string(), Arc::new(MockStorageFactory::kafka()));
assert_eq!(registry.list().len(), 2);
}
#[test]
fn storage_registry_resolve_covers_both_known_and_extension() {
let mut registry = StorageRegistry::new();
registry.register_known(StoragePlatform::S3, Arc::new(MockStorageFactory::s3()));
registry.register_extension("kafka".to_string(), Arc::new(MockStorageFactory::kafka()));
assert!(registry.resolve("s3").is_some());
assert!(registry.resolve("kafka").is_some());
assert!(registry.resolve("nonexistent").is_none());
}
}