feagi_evolutionary/
storage.rs1use core::future::Future;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum StorageError {
23 NotFound,
25 IOError(String),
27 SerializationError(String),
29}
30
31impl std::fmt::Display for StorageError {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 StorageError::NotFound => write!(f, "Genome not found"),
35 StorageError::IOError(msg) => write!(f, "I/O error: {}", msg),
36 StorageError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
37 }
38 }
39}
40
41impl std::error::Error for StorageError {}
42
43pub trait GenomeStorage: Send + Sync {
52 fn load_genome(
64 &self,
65 genome_id: &str,
66 ) -> Pin<Box<dyn Future<Output = Result<String, StorageError>> + Send + '_>>;
67
68 fn save_genome(
81 &self,
82 genome_id: &str,
83 genome_json: &str,
84 ) -> Pin<Box<dyn Future<Output = Result<(), StorageError>> + Send + '_>>;
85
86 fn list_genomes(
93 &self,
94 ) -> Pin<Box<dyn Future<Output = Result<Vec<String>, StorageError>> + Send + '_>>;
95
96 fn delete_genome(
108 &self,
109 genome_id: &str,
110 ) -> Pin<Box<dyn Future<Output = Result<(), StorageError>> + Send + '_>>;
111}
112
113use core::pin::Pin;
115
116#[cfg(feature = "async-tokio")]
117pub mod fs_storage {
118 use super::{GenomeStorage, StorageError};
123 use core::future::Future;
124 use core::pin::Pin;
125 use std::path::{Path, PathBuf};
126
127 pub struct FileSystemStorage {
137 base_path: PathBuf,
138 }
139
140 impl FileSystemStorage {
141 pub fn new<P: AsRef<Path>>(base_path: P) -> Result<Self, StorageError> {
151 let path = base_path.as_ref().to_path_buf();
152
153 std::fs::create_dir_all(&path)
155 .map_err(|e| StorageError::IOError(format!("Failed to create directory: {}", e)))?;
156
157 Ok(Self { base_path: path })
158 }
159
160 fn genome_path(&self, genome_id: &str) -> PathBuf {
162 let sanitized = genome_id
164 .chars()
165 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
166 .collect::<String>();
167
168 self.base_path.join(format!("{}.json", sanitized))
169 }
170 }
171
172 #[cfg(feature = "async-tokio")]
173 impl GenomeStorage for FileSystemStorage {
174 fn load_genome(
175 &self,
176 genome_id: &str,
177 ) -> Pin<Box<dyn Future<Output = Result<String, StorageError>> + Send + '_>> {
178 let path = self.genome_path(genome_id);
179 Box::pin(async move {
180 tokio::fs::read_to_string(&path).await.map_err(|e| {
181 if e.kind() == std::io::ErrorKind::NotFound {
182 StorageError::NotFound
183 } else {
184 StorageError::IOError(format!("Failed to read file: {}", e))
185 }
186 })
187 })
188 }
189
190 fn save_genome(
191 &self,
192 genome_id: &str,
193 genome_json: &str,
194 ) -> Pin<Box<dyn Future<Output = Result<(), StorageError>> + Send + '_>> {
195 let path = self.genome_path(genome_id);
196 let json = genome_json.to_string();
197
198 Box::pin(async move {
199 serde_json::from_str::<serde_json::Value>(&json).map_err(|e| {
201 StorageError::SerializationError(format!("Invalid JSON: {}", e))
202 })?;
203
204 tokio::fs::write(&path, json)
205 .await
206 .map_err(|e| StorageError::IOError(format!("Failed to write file: {}", e)))?;
207
208 Ok(())
209 })
210 }
211
212 fn list_genomes(
213 &self,
214 ) -> Pin<Box<dyn Future<Output = Result<Vec<String>, StorageError>> + Send + '_>> {
215 let base_path = self.base_path.clone();
216 Box::pin(async move {
217 let mut entries = tokio::fs::read_dir(&base_path).await.map_err(|e| {
218 StorageError::IOError(format!("Failed to read directory: {}", e))
219 })?;
220
221 let mut genome_ids = Vec::new();
222 while let Some(entry) = entries.next_entry().await.map_err(|e| {
223 StorageError::IOError(format!("Failed to read directory entry: {}", e))
224 })? {
225 let path = entry.path();
226 if path.extension().and_then(|s| s.to_str()) == Some("json") {
227 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
228 genome_ids.push(stem.to_string());
229 }
230 }
231 }
232
233 Ok(genome_ids)
234 })
235 }
236
237 fn delete_genome(
238 &self,
239 genome_id: &str,
240 ) -> Pin<Box<dyn Future<Output = Result<(), StorageError>> + Send + '_>> {
241 let path = self.genome_path(genome_id);
242 Box::pin(async move {
243 tokio::fs::remove_file(&path).await.map_err(|e| {
244 if e.kind() == std::io::ErrorKind::NotFound {
245 StorageError::NotFound
246 } else {
247 StorageError::IOError(format!("Failed to delete file: {}", e))
248 }
249 })
250 })
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 #[cfg(feature = "async-tokio")]
258 use super::fs_storage::FileSystemStorage;
259
260 #[cfg(feature = "async-tokio")]
261 #[tokio::test]
262 async fn test_filesystem_storage() {
263 use tempfile::TempDir;
264
265 let temp_dir = TempDir::new().unwrap();
266 let storage = FileSystemStorage::new(temp_dir.path()).unwrap();
267
268 let genome_id = "test_genome";
270 let genome_json = r#"{"genome_id": "test_genome", "version": "2.1"}"#;
271 storage.save_genome(genome_id, genome_json).await.unwrap();
272
273 let loaded = storage.load_genome(genome_id).await.unwrap();
275 assert_eq!(loaded, genome_json);
276
277 let genomes = storage.list_genomes().await.unwrap();
279 assert!(genomes.contains(&genome_id.to_string()));
280
281 storage.delete_genome(genome_id).await.unwrap();
283
284 let result = storage.load_genome(genome_id).await;
286 assert!(matches!(result, Err(StorageError::NotFound)));
287 }
288}