use core::future::Future;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StorageError {
NotFound,
IOError(String),
SerializationError(String),
}
impl std::fmt::Display for StorageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StorageError::NotFound => write!(f, "Genome not found"),
StorageError::IOError(msg) => write!(f, "I/O error: {}", msg),
StorageError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
}
}
}
impl std::error::Error for StorageError {}
pub trait GenomeStorage: Send + Sync {
fn load_genome(
&self,
genome_id: &str,
) -> Pin<Box<dyn Future<Output = Result<String, StorageError>> + Send + '_>>;
fn save_genome(
&self,
genome_id: &str,
genome_json: &str,
) -> Pin<Box<dyn Future<Output = Result<(), StorageError>> + Send + '_>>;
fn list_genomes(
&self,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, StorageError>> + Send + '_>>;
fn delete_genome(
&self,
genome_id: &str,
) -> Pin<Box<dyn Future<Output = Result<(), StorageError>> + Send + '_>>;
}
use core::pin::Pin;
#[cfg(feature = "async-tokio")]
pub mod fs_storage {
use super::{GenomeStorage, StorageError};
use core::future::Future;
use core::pin::Pin;
use std::path::{Path, PathBuf};
pub struct FileSystemStorage {
base_path: PathBuf,
}
impl FileSystemStorage {
pub fn new<P: AsRef<Path>>(base_path: P) -> Result<Self, StorageError> {
let path = base_path.as_ref().to_path_buf();
std::fs::create_dir_all(&path)
.map_err(|e| StorageError::IOError(format!("Failed to create directory: {}", e)))?;
Ok(Self { base_path: path })
}
fn genome_path(&self, genome_id: &str) -> PathBuf {
let sanitized = genome_id
.chars()
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
.collect::<String>();
self.base_path.join(format!("{}.json", sanitized))
}
}
#[cfg(feature = "async-tokio")]
impl GenomeStorage for FileSystemStorage {
fn load_genome(
&self,
genome_id: &str,
) -> Pin<Box<dyn Future<Output = Result<String, StorageError>> + Send + '_>> {
let path = self.genome_path(genome_id);
Box::pin(async move {
tokio::fs::read_to_string(&path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StorageError::NotFound
} else {
StorageError::IOError(format!("Failed to read file: {}", e))
}
})
})
}
fn save_genome(
&self,
genome_id: &str,
genome_json: &str,
) -> Pin<Box<dyn Future<Output = Result<(), StorageError>> + Send + '_>> {
let path = self.genome_path(genome_id);
let json = genome_json.to_string();
Box::pin(async move {
serde_json::from_str::<serde_json::Value>(&json).map_err(|e| {
StorageError::SerializationError(format!("Invalid JSON: {}", e))
})?;
tokio::fs::write(&path, json)
.await
.map_err(|e| StorageError::IOError(format!("Failed to write file: {}", e)))?;
Ok(())
})
}
fn list_genomes(
&self,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, StorageError>> + Send + '_>> {
let base_path = self.base_path.clone();
Box::pin(async move {
let mut entries = tokio::fs::read_dir(&base_path).await.map_err(|e| {
StorageError::IOError(format!("Failed to read directory: {}", e))
})?;
let mut genome_ids = Vec::new();
while let Some(entry) = entries.next_entry().await.map_err(|e| {
StorageError::IOError(format!("Failed to read directory entry: {}", e))
})? {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
genome_ids.push(stem.to_string());
}
}
}
Ok(genome_ids)
})
}
fn delete_genome(
&self,
genome_id: &str,
) -> Pin<Box<dyn Future<Output = Result<(), StorageError>> + Send + '_>> {
let path = self.genome_path(genome_id);
Box::pin(async move {
tokio::fs::remove_file(&path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StorageError::NotFound
} else {
StorageError::IOError(format!("Failed to delete file: {}", e))
}
})
})
}
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "async-tokio")]
use super::fs_storage::FileSystemStorage;
#[cfg(feature = "async-tokio")]
#[tokio::test]
async fn test_filesystem_storage() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let storage = FileSystemStorage::new(temp_dir.path()).unwrap();
let genome_id = "test_genome";
let genome_json = r#"{"genome_id": "test_genome", "version": "2.1"}"#;
storage.save_genome(genome_id, genome_json).await.unwrap();
let loaded = storage.load_genome(genome_id).await.unwrap();
assert_eq!(loaded, genome_json);
let genomes = storage.list_genomes().await.unwrap();
assert!(genomes.contains(&genome_id.to_string()));
storage.delete_genome(genome_id).await.unwrap();
let result = storage.load_genome(genome_id).await;
assert!(matches!(result, Err(StorageError::NotFound)));
}
}