Skip to main content

backbone_core/persistence/
memory.rs

1//! In-Memory Repository Implementation
2//!
3//! Thread-safe in-memory storage for entities. Useful for:
4//! - Unit testing
5//! - Prototyping
6//! - Development without database
7//! - Entities that don't need persistence
8
9use async_trait::async_trait;
10use chrono::Utc;
11use std::collections::HashMap;
12use std::sync::RwLock;
13
14use super::traits::{CrudRepository, PersistentEntity, RepositoryError, SearchableRepository};
15
16/// Generic in-memory repository
17///
18/// Provides thread-safe storage for any entity implementing `PersistentEntity`.
19/// All data is stored in memory and lost when the application restarts.
20///
21/// # Example
22///
23/// ```ignore
24/// use backbone_core::persistence::{InMemoryRepository, PersistentEntity};
25///
26/// #[derive(Clone, Debug, Serialize, Deserialize)]
27/// struct User {
28///     id: String,
29///     name: String,
30///     created_at: Option<DateTime<Utc>>,
31///     updated_at: Option<DateTime<Utc>>,
32///     deleted_at: Option<DateTime<Utc>>,
33/// }
34///
35/// impl PersistentEntity for User { /* ... */ }
36///
37/// let repo = InMemoryRepository::<User>::new();
38/// ```
39pub struct InMemoryRepository<E>
40where
41    E: PersistentEntity,
42{
43    /// Active entities (not soft-deleted)
44    store: RwLock<HashMap<String, E>>,
45    /// Soft-deleted entities (trash)
46    trash: RwLock<HashMap<String, E>>,
47}
48
49impl<E> InMemoryRepository<E>
50where
51    E: PersistentEntity,
52{
53    /// Create a new empty repository
54    pub fn new() -> Self {
55        Self {
56            store: RwLock::new(HashMap::new()),
57            trash: RwLock::new(HashMap::new()),
58        }
59    }
60
61    /// Create a repository with initial data
62    pub fn with_data(entities: Vec<E>) -> Self {
63        let store: HashMap<String, E> = entities
64            .into_iter()
65            .map(|e| (e.entity_id(), e))
66            .collect();
67
68        Self {
69            store: RwLock::new(store),
70            trash: RwLock::new(HashMap::new()),
71        }
72    }
73
74    /// Get the number of active entities
75    pub fn len(&self) -> usize {
76        self.store.read().unwrap().len()
77    }
78
79    /// Check if store is empty
80    pub fn is_empty(&self) -> bool {
81        self.store.read().unwrap().is_empty()
82    }
83
84    /// Get the number of items in trash
85    pub fn trash_len(&self) -> usize {
86        self.trash.read().unwrap().len()
87    }
88
89    /// Clear all data (both active and trash)
90    pub fn clear(&self) {
91        self.store.write().unwrap().clear();
92        self.trash.write().unwrap().clear();
93    }
94}
95
96impl<E> Default for InMemoryRepository<E>
97where
98    E: PersistentEntity,
99{
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105#[async_trait]
106impl<E> CrudRepository<E> for InMemoryRepository<E>
107where
108    E: PersistentEntity,
109{
110    async fn create(&self, mut entity: E) -> Result<E, RepositoryError> {
111        let mut store = self.store.write().unwrap();
112
113        // Generate ID if empty
114        if entity.entity_id().is_empty() {
115            entity.set_entity_id(E::generate_id());
116        }
117
118        // Check for duplicates
119        if store.contains_key(&entity.entity_id()) {
120            return Err(RepositoryError::AlreadyExists(format!(
121                "Entity with ID {} already exists",
122                entity.entity_id()
123            )));
124        }
125
126        // Set timestamps
127        let now = Utc::now();
128        if entity.created_at().is_none() {
129            entity.set_created_at(now);
130        }
131        entity.set_updated_at(now);
132
133        let id = entity.entity_id();
134        store.insert(id, entity.clone());
135        Ok(entity)
136    }
137
138    async fn find_by_id(&self, id: &str) -> Result<Option<E>, RepositoryError> {
139        let store = self.store.read().unwrap();
140        Ok(store.get(id).cloned())
141    }
142
143    async fn find_by_id_including_deleted(&self, id: &str) -> Result<Option<E>, RepositoryError> {
144        // Check active store first
145        {
146            let store = self.store.read().unwrap();
147            if let Some(entity) = store.get(id) {
148                return Ok(Some(entity.clone()));
149            }
150        }
151
152        // Check trash
153        let trash = self.trash.read().unwrap();
154        Ok(trash.get(id).cloned())
155    }
156
157    async fn update(&self, mut entity: E) -> Result<E, RepositoryError> {
158        let mut store = self.store.write().unwrap();
159
160        if !store.contains_key(&entity.entity_id()) {
161            return Err(RepositoryError::NotFound);
162        }
163
164        entity.set_updated_at(Utc::now());
165        let id = entity.entity_id();
166        store.insert(id, entity.clone());
167        Ok(entity)
168    }
169
170    async fn soft_delete(&self, id: &str) -> Result<bool, RepositoryError> {
171        let mut store = self.store.write().unwrap();
172        let mut trash = self.trash.write().unwrap();
173
174        if let Some(mut entity) = store.remove(id) {
175            entity.mark_deleted();
176            trash.insert(id.to_string(), entity);
177            return Ok(true);
178        }
179
180        Ok(false)
181    }
182
183    async fn restore(&self, id: &str) -> Result<Option<E>, RepositoryError> {
184        let mut store = self.store.write().unwrap();
185        let mut trash = self.trash.write().unwrap();
186
187        if let Some(mut entity) = trash.remove(id) {
188            entity.restore();
189            store.insert(id.to_string(), entity.clone());
190            return Ok(Some(entity));
191        }
192
193        Ok(None)
194    }
195
196    async fn hard_delete(&self, id: &str) -> Result<bool, RepositoryError> {
197        let mut store = self.store.write().unwrap();
198        let mut trash = self.trash.write().unwrap();
199
200        // Try to remove from both stores
201        let removed_active = store.remove(id).is_some();
202        let removed_trash = trash.remove(id).is_some();
203
204        Ok(removed_active || removed_trash)
205    }
206
207    async fn list(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError> {
208        let store = self.store.read().unwrap();
209        let total = store.len() as u64;
210
211        let offset = ((page.saturating_sub(1)) * limit) as usize;
212        let entities: Vec<E> = store
213            .values()
214            .skip(offset)
215            .take(limit as usize)
216            .cloned()
217            .collect();
218
219        Ok((entities, total))
220    }
221
222    async fn list_deleted(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError> {
223        let trash = self.trash.read().unwrap();
224        let total = trash.len() as u64;
225
226        let offset = ((page.saturating_sub(1)) * limit) as usize;
227        let entities: Vec<E> = trash
228            .values()
229            .skip(offset)
230            .take(limit as usize)
231            .cloned()
232            .collect();
233
234        Ok((entities, total))
235    }
236
237    async fn count(&self) -> Result<u64, RepositoryError> {
238        Ok(self.store.read().unwrap().len() as u64)
239    }
240
241    async fn count_deleted(&self) -> Result<u64, RepositoryError> {
242        Ok(self.trash.read().unwrap().len() as u64)
243    }
244
245    async fn bulk_create(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError> {
246        let mut results = Vec::with_capacity(entities.len());
247        for entity in entities {
248            results.push(self.create(entity).await?);
249        }
250        Ok(results)
251    }
252
253    async fn empty_trash(&self) -> Result<u64, RepositoryError> {
254        let mut trash = self.trash.write().unwrap();
255        let count = trash.len() as u64;
256        trash.clear();
257        Ok(count)
258    }
259}
260
261#[async_trait]
262impl<E> SearchableRepository<E> for InMemoryRepository<E>
263where
264    E: PersistentEntity,
265{
266    async fn search(
267        &self,
268        filters: HashMap<String, String>,
269        page: u32,
270        limit: u32,
271    ) -> Result<(Vec<E>, u64), RepositoryError> {
272        // For in-memory, we do basic JSON field matching
273        let store = self.store.read().unwrap();
274
275        let filtered: Vec<E> = store
276            .values()
277            .filter(|entity| {
278                // Serialize entity to JSON for field matching
279                if let Ok(json) = serde_json::to_value(entity) {
280                    filters.iter().all(|(key, value)| {
281                        if let Some(field_value) = json.get(key) {
282                            match field_value {
283                                serde_json::Value::String(s) => {
284                                    s.to_lowercase().contains(&value.to_lowercase())
285                                }
286                                serde_json::Value::Number(n) => n.to_string() == *value,
287                                serde_json::Value::Bool(b) => b.to_string() == *value,
288                                _ => false,
289                            }
290                        } else {
291                            false
292                        }
293                    })
294                } else {
295                    false
296                }
297            })
298            .cloned()
299            .collect();
300
301        let total = filtered.len() as u64;
302        let offset = ((page.saturating_sub(1)) * limit) as usize;
303        let paginated: Vec<E> = filtered
304            .into_iter()
305            .skip(offset)
306            .take(limit as usize)
307            .collect();
308
309        Ok((paginated, total))
310    }
311
312    async fn find_by_field(&self, field: &str, value: &str) -> Result<Option<E>, RepositoryError> {
313        let store = self.store.read().unwrap();
314
315        for entity in store.values() {
316            if let Ok(json) = serde_json::to_value(entity) {
317                if let Some(field_value) = json.get(field) {
318                    let matches = match field_value {
319                        serde_json::Value::String(s) => s == value,
320                        serde_json::Value::Number(n) => n.to_string() == value,
321                        serde_json::Value::Bool(b) => b.to_string() == value,
322                        _ => false,
323                    };
324                    if matches {
325                        return Ok(Some(entity.clone()));
326                    }
327                }
328            }
329        }
330
331        Ok(None)
332    }
333
334    async fn find_all_by_field(
335        &self,
336        field: &str,
337        value: &str,
338        page: u32,
339        limit: u32,
340    ) -> Result<(Vec<E>, u64), RepositoryError> {
341        let mut filters = HashMap::new();
342        filters.insert(field.to_string(), value.to_string());
343        self.search(filters, page, limit).await
344    }
345}
346
347// ============================================================
348// Tests
349// ============================================================
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use chrono::Utc;
355    use serde::{Deserialize, Serialize};
356
357    #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
358    struct TestEntity {
359        id: String,
360        name: String,
361        value: i32,
362        created_at: Option<chrono::DateTime<Utc>>,
363        updated_at: Option<chrono::DateTime<Utc>>,
364        deleted_at: Option<chrono::DateTime<Utc>>,
365    }
366
367    impl TestEntity {
368        fn new(name: &str, value: i32) -> Self {
369            Self {
370                id: String::new(),
371                name: name.to_string(),
372                value,
373                created_at: None,
374                updated_at: None,
375                deleted_at: None,
376            }
377        }
378    }
379
380    impl PersistentEntity for TestEntity {
381        fn entity_id(&self) -> String {
382            self.id.clone()
383        }
384
385        fn set_entity_id(&mut self, id: String) {
386            self.id = id;
387        }
388
389        fn created_at(&self) -> Option<chrono::DateTime<Utc>> {
390            self.created_at
391        }
392
393        fn set_created_at(&mut self, ts: chrono::DateTime<Utc>) {
394            self.created_at = Some(ts);
395        }
396
397        fn updated_at(&self) -> Option<chrono::DateTime<Utc>> {
398            self.updated_at
399        }
400
401        fn set_updated_at(&mut self, ts: chrono::DateTime<Utc>) {
402            self.updated_at = Some(ts);
403        }
404
405        fn deleted_at(&self) -> Option<chrono::DateTime<Utc>> {
406            self.deleted_at
407        }
408
409        fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<Utc>>) {
410            self.deleted_at = ts;
411        }
412    }
413
414    #[tokio::test]
415    async fn test_create_and_find() {
416        let repo = InMemoryRepository::<TestEntity>::new();
417        let entity = TestEntity::new("test", 42);
418
419        let created = repo.create(entity).await.unwrap();
420        assert!(!created.id.is_empty());
421        assert_eq!(created.name, "test");
422        assert!(created.created_at.is_some());
423
424        let found = repo.find_by_id(&created.id).await.unwrap();
425        assert!(found.is_some());
426        assert_eq!(found.unwrap().name, "test");
427    }
428
429    #[tokio::test]
430    async fn test_soft_delete_and_restore() {
431        let repo = InMemoryRepository::<TestEntity>::new();
432        let entity = TestEntity::new("test", 42);
433
434        let created = repo.create(entity).await.unwrap();
435        let id = created.id.clone();
436
437        // Soft delete
438        assert!(repo.soft_delete(&id).await.unwrap());
439        assert!(repo.find_by_id(&id).await.unwrap().is_none());
440        assert!(repo.find_by_id_including_deleted(&id).await.unwrap().is_some());
441
442        // Restore
443        let restored = repo.restore(&id).await.unwrap();
444        assert!(restored.is_some());
445        assert!(repo.find_by_id(&id).await.unwrap().is_some());
446    }
447
448    #[tokio::test]
449    async fn test_bulk_create() {
450        let repo = InMemoryRepository::<TestEntity>::new();
451        let entities = vec![
452            TestEntity::new("one", 1),
453            TestEntity::new("two", 2),
454            TestEntity::new("three", 3),
455        ];
456
457        let created = repo.bulk_create(entities).await.unwrap();
458        assert_eq!(created.len(), 3);
459        assert_eq!(repo.count().await.unwrap(), 3);
460    }
461
462    #[tokio::test]
463    async fn test_pagination() {
464        let repo = InMemoryRepository::<TestEntity>::new();
465
466        // Create 10 entities
467        for i in 0..10 {
468            repo.create(TestEntity::new(&format!("entity_{}", i), i))
469                .await
470                .unwrap();
471        }
472
473        // Test pagination
474        let (page1, total) = repo.list(1, 3).await.unwrap();
475        assert_eq!(total, 10);
476        assert_eq!(page1.len(), 3);
477
478        let (page2, _) = repo.list(2, 3).await.unwrap();
479        assert_eq!(page2.len(), 3);
480    }
481
482    #[tokio::test]
483    async fn test_search() {
484        let repo = InMemoryRepository::<TestEntity>::new();
485
486        repo.create(TestEntity::new("apple", 1)).await.unwrap();
487        repo.create(TestEntity::new("banana", 2)).await.unwrap();
488        repo.create(TestEntity::new("apple pie", 3)).await.unwrap();
489
490        let mut filters = HashMap::new();
491        filters.insert("name".to_string(), "apple".to_string());
492
493        let (results, total) = repo.search(filters, 1, 10).await.unwrap();
494        assert_eq!(total, 2);
495        assert_eq!(results.len(), 2);
496    }
497}