backbone_core/persistence/
memory.rs1use async_trait::async_trait;
10use chrono::Utc;
11use std::collections::HashMap;
12use std::sync::RwLock;
13
14use super::traits::{CrudRepository, PersistentEntity, RepositoryError, SearchableRepository};
15
16pub struct InMemoryRepository<E>
40where
41 E: PersistentEntity,
42{
43 store: RwLock<HashMap<String, E>>,
45 trash: RwLock<HashMap<String, E>>,
47}
48
49fn 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 pub fn new() -> Self {
64 Self {
65 store: RwLock::new(HashMap::new()),
66 trash: RwLock::new(HashMap::new()),
67 }
68 }
69
70 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 pub fn len(&self) -> usize {
85 self.store.read().unwrap().len()
86 }
87
88 pub fn is_empty(&self) -> bool {
90 self.store.read().unwrap().is_empty()
91 }
92
93 pub fn trash_len(&self) -> usize {
95 self.trash.read().unwrap().len()
96 }
97
98 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 if entity.entity_id().is_empty() {
124 entity.set_entity_id(E::generate_id());
125 }
126
127 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 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 {
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 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 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 let store = self.store.read().unwrap();
283
284 let filtered: Vec<E> = ordered_by_id(&store)
285 .into_iter()
286 .filter(|entity| {
287 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#[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 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 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 for i in 0..10 {
503 repo.create(TestEntity::new(&format!("entity_{}", i), i))
504 .await
505 .unwrap();
506 }
507
508 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}