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