Skip to main content

backbone_orm/
in_memory.rs

1//! In-Memory Store - Generic Testing and Prototyping Utility
2//!
3//! Provides a thread-safe, async-compatible in-memory storage implementation
4//! for use in testing, prototyping, and development environments.
5//!
6//! # Features
7//!
8//! - Generic storage for any `Clone + Send + Sync` type
9//! - Soft delete with trash/restore functionality
10//! - Pagination support
11//! - Thread-safe using `RwLock`
12//! - Async-compatible
13//!
14//! # Example
15//!
16//! ```rust,ignore
17//! use backbone_orm::InMemoryStore;
18//!
19//! #[derive(Clone)]
20//! struct User {
21//!     id: String,
22//!     name: String,
23//! }
24//!
25//! #[tokio::main]
26//! async fn main() {
27//!     let store = InMemoryStore::<User>::new();
28//!
29//!     // Insert
30//!     let user = User { id: "1".to_string(), name: "Alice".to_string() };
31//!     store.insert("1".to_string(), user.clone()).await;
32//!
33//!     // Get
34//!     let found = store.get("1").await;
35//!     assert!(found.is_some());
36//!
37//!     // Soft delete
38//!     store.soft_delete("1").await;
39//!     assert!(store.get("1").await.is_none());
40//!
41//!     // Restore
42//!     store.restore("1").await;
43//!     assert!(store.get("1").await.is_some());
44//! }
45//! ```
46
47use std::collections::HashMap;
48use std::sync::Arc;
49use tokio::sync::RwLock;
50
51/// Generic in-memory store for entities
52///
53/// This store provides a simple key-value storage with soft delete support.
54/// It's designed for testing and prototyping, not for production use.
55///
56/// # Type Parameters
57///
58/// - `T`: The entity type to store. Must be `Clone + Send + Sync`.
59pub struct InMemoryStore<T: Clone + Send + Sync> {
60    /// Active items
61    items: RwLock<HashMap<String, T>>,
62    /// Soft-deleted items (trash)
63    deleted: RwLock<HashMap<String, T>>,
64}
65
66impl<T: Clone + Send + Sync> InMemoryStore<T> {
67    /// Create a new empty in-memory store
68    pub fn new() -> Self {
69        Self {
70            items: RwLock::new(HashMap::new()),
71            deleted: RwLock::new(HashMap::new()),
72        }
73    }
74
75    /// Create a new store wrapped in Arc for sharing across async tasks
76    pub fn new_shared() -> Arc<Self> {
77        Arc::new(Self::new())
78    }
79
80    // ========================================================================
81    // Basic CRUD Operations
82    // ========================================================================
83
84    /// Insert or update an item
85    pub async fn insert(&self, id: String, item: T) {
86        self.items.write().await.insert(id, item);
87    }
88
89    /// Get an item by ID (returns None if not found or soft-deleted)
90    pub async fn get(&self, id: &str) -> Option<T> {
91        self.items.read().await.get(id).cloned()
92    }
93
94    /// Get a soft-deleted item by ID
95    pub async fn get_deleted(&self, id: &str) -> Option<T> {
96        self.deleted.read().await.get(id).cloned()
97    }
98
99    /// Check if an item exists (excluding soft-deleted)
100    pub async fn exists(&self, id: &str) -> bool {
101        self.items.read().await.contains_key(id)
102    }
103
104    /// Check if an item exists in trash
105    pub async fn exists_in_trash(&self, id: &str) -> bool {
106        self.deleted.read().await.contains_key(id)
107    }
108
109    /// Update an existing item (returns None if not found)
110    pub async fn update(&self, id: &str, item: T) -> Option<T> {
111        let mut items = self.items.write().await;
112        if items.contains_key(id) {
113            items.insert(id.to_string(), item.clone());
114            Some(item)
115        } else {
116            None
117        }
118    }
119
120    /// Remove an item permanently (hard delete)
121    pub async fn remove(&self, id: &str) -> Option<T> {
122        self.items.write().await.remove(id)
123    }
124
125    // ========================================================================
126    // Soft Delete Operations
127    // ========================================================================
128
129    /// Soft delete an item (move to trash)
130    pub async fn soft_delete(&self, id: &str) -> bool {
131        let mut items = self.items.write().await;
132        let mut deleted = self.deleted.write().await;
133        if let Some(item) = items.remove(id) {
134            deleted.insert(id.to_string(), item);
135            true
136        } else {
137            false
138        }
139    }
140
141    /// Restore a soft-deleted item from trash
142    pub async fn restore(&self, id: &str) -> Option<T> {
143        let mut items = self.items.write().await;
144        let mut deleted = self.deleted.write().await;
145        if let Some(item) = deleted.remove(id) {
146            items.insert(id.to_string(), item.clone());
147            Some(item)
148        } else {
149            None
150        }
151    }
152
153    /// Permanently delete an item from trash
154    pub async fn hard_delete(&self, id: &str) -> Option<T> {
155        self.deleted.write().await.remove(id)
156    }
157
158    /// Empty the trash (permanently delete all soft-deleted items)
159    pub async fn empty_trash(&self) -> u64 {
160        let mut deleted = self.deleted.write().await;
161        let count = deleted.len() as u64;
162        deleted.clear();
163        count
164    }
165
166    // ========================================================================
167    // List Operations
168    // ========================================================================
169
170    /// List items with pagination
171    ///
172    /// # Arguments
173    ///
174    /// - `page`: Page number (1-indexed)
175    /// - `limit`: Items per page
176    ///
177    /// # Returns
178    ///
179    /// Tuple of (items, total_count)
180    pub async fn list(&self, page: u32, limit: u32) -> (Vec<T>, u64) {
181        let items = self.items.read().await;
182        let total = items.len() as u64;
183        let skip = ((page.saturating_sub(1)) * limit) as usize;
184        let items: Vec<T> = items.values().skip(skip).take(limit as usize).cloned().collect();
185        (items, total)
186    }
187
188    /// List soft-deleted items with pagination
189    pub async fn list_deleted(&self, page: u32, limit: u32) -> (Vec<T>, u64) {
190        let deleted = self.deleted.read().await;
191        let total = deleted.len() as u64;
192        let skip = ((page.saturating_sub(1)) * limit) as usize;
193        let items: Vec<T> = deleted.values().skip(skip).take(limit as usize).cloned().collect();
194        (items, total)
195    }
196
197    /// Get all items (no pagination)
198    pub async fn all(&self) -> Vec<T> {
199        self.items.read().await.values().cloned().collect()
200    }
201
202    /// Get all soft-deleted items
203    pub async fn all_deleted(&self) -> Vec<T> {
204        self.deleted.read().await.values().cloned().collect()
205    }
206
207    // ========================================================================
208    // Utility Operations
209    // ========================================================================
210
211    /// Get the count of active items
212    pub async fn count(&self) -> u64 {
213        self.items.read().await.len() as u64
214    }
215
216    /// Get the count of soft-deleted items
217    pub async fn trash_count(&self) -> u64 {
218        self.deleted.read().await.len() as u64
219    }
220
221    /// Clear all items (both active and deleted)
222    pub async fn clear(&self) {
223        self.items.write().await.clear();
224        self.deleted.write().await.clear();
225    }
226
227    /// Clear only active items
228    pub async fn clear_active(&self) {
229        self.items.write().await.clear();
230    }
231
232    /// Get all IDs of active items
233    pub async fn ids(&self) -> Vec<String> {
234        self.items.read().await.keys().cloned().collect()
235    }
236
237    /// Get all IDs of soft-deleted items
238    pub async fn trash_ids(&self) -> Vec<String> {
239        self.deleted.read().await.keys().cloned().collect()
240    }
241}
242
243impl<T: Clone + Send + Sync> Default for InMemoryStore<T> {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249// ============================================================================
250// Tests
251// ============================================================================
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[derive(Debug, Clone, PartialEq)]
258    struct TestEntity {
259        id: String,
260        name: String,
261    }
262
263    #[tokio::test]
264    async fn test_insert_and_get() {
265        let store = InMemoryStore::<TestEntity>::new();
266        let entity = TestEntity {
267            id: "1".to_string(),
268            name: "Test".to_string(),
269        };
270
271        store.insert("1".to_string(), entity.clone()).await;
272
273        let found = store.get("1").await;
274        assert!(found.is_some());
275        assert_eq!(found.unwrap().name, "Test");
276    }
277
278    #[tokio::test]
279    async fn test_update() {
280        let store = InMemoryStore::<TestEntity>::new();
281        let entity = TestEntity {
282            id: "1".to_string(),
283            name: "Original".to_string(),
284        };
285
286        store.insert("1".to_string(), entity).await;
287
288        let updated = TestEntity {
289            id: "1".to_string(),
290            name: "Updated".to_string(),
291        };
292
293        let result = store.update("1", updated).await;
294        assert!(result.is_some());
295
296        let found = store.get("1").await;
297        assert_eq!(found.unwrap().name, "Updated");
298    }
299
300    #[tokio::test]
301    async fn test_soft_delete_and_restore() {
302        let store = InMemoryStore::<TestEntity>::new();
303        let entity = TestEntity {
304            id: "1".to_string(),
305            name: "Test".to_string(),
306        };
307
308        store.insert("1".to_string(), entity).await;
309
310        // Soft delete
311        let deleted = store.soft_delete("1").await;
312        assert!(deleted);
313        assert!(store.get("1").await.is_none());
314        assert!(store.get_deleted("1").await.is_some());
315
316        // Restore
317        let restored = store.restore("1").await;
318        assert!(restored.is_some());
319        assert!(store.get("1").await.is_some());
320        assert!(store.get_deleted("1").await.is_none());
321    }
322
323    #[tokio::test]
324    async fn test_list_pagination() {
325        let store = InMemoryStore::<TestEntity>::new();
326
327        for i in 0..10 {
328            let entity = TestEntity {
329                id: i.to_string(),
330                name: format!("Entity {}", i),
331            };
332            store.insert(i.to_string(), entity).await;
333        }
334
335        let (items, total) = store.list(1, 3).await;
336        assert_eq!(items.len(), 3);
337        assert_eq!(total, 10);
338
339        let (items, total) = store.list(4, 3).await;
340        assert_eq!(items.len(), 1);
341        assert_eq!(total, 10);
342    }
343
344    #[tokio::test]
345    async fn test_empty_trash() {
346        let store = InMemoryStore::<TestEntity>::new();
347
348        for i in 0..5 {
349            let entity = TestEntity {
350                id: i.to_string(),
351                name: format!("Entity {}", i),
352            };
353            store.insert(i.to_string(), entity).await;
354            store.soft_delete(&i.to_string()).await;
355        }
356
357        assert_eq!(store.trash_count().await, 5);
358
359        let emptied = store.empty_trash().await;
360        assert_eq!(emptied, 5);
361        assert_eq!(store.trash_count().await, 0);
362    }
363
364    #[tokio::test]
365    async fn test_exists() {
366        let store = InMemoryStore::<TestEntity>::new();
367        let entity = TestEntity {
368            id: "1".to_string(),
369            name: "Test".to_string(),
370        };
371
372        store.insert("1".to_string(), entity).await;
373
374        assert!(store.exists("1").await);
375        assert!(!store.exists("2").await);
376
377        store.soft_delete("1").await;
378        assert!(!store.exists("1").await);
379        assert!(store.exists_in_trash("1").await);
380    }
381}