//! # memory //! //! This crate provides in-memory implementations of the `Repository` trait for any entity type. //! It is primarily intended for testing, development, and CLI/TUI modes where persistence is not required. //! //! ## Features //! - Thread-safe, async in-memory storage for any entity type //! - Implements all CRUD operations //! - Used as a backend for memory-based API, CLI, and TUI binaries //! - Comprehensive unit tests for all repository and service operations //! //! ## Usage //! Use `InMemoryUserRepository` and `InMemoryProductRepository` directly, or via the `MemoryUserService` and `MemoryProductService` type aliases. /* * This file is part of Sharenet. * * Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. * * You may obtain a copy of the license at: * https://creativecommons.org/licenses/by-nc-sa/4.0/ * * Copyright (c) 2024 Continuist */ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; use domain::{ CreateProduct, CreateUser, Product, Result, UpdateProduct, UpdateUser, User, Entity, }; use application::{Repository, Service}; /// Generic trait for entities that can be stored in memory pub trait MemoryEntity: Entity + Clone + Send + Sync { /// Get the entity's ID fn id(&self) -> Uuid; /// Create a new entity from create data fn from_create_data(data: Self::Create) -> Result; /// Update the entity with update data fn update(&mut self, data: Self::Update) -> Result<()>; /// Get the entity name for error messages fn entity_name() -> &'static str; } /// Generic, thread-safe, async in-memory repository for any entity type. /// /// Implements all CRUD operations. Intended for testing and non-persistent use cases. #[derive(Default, Clone)] pub struct InMemoryRepository { entities: Arc>>, } impl InMemoryRepository { pub fn new() -> Self { Self { entities: Arc::new(RwLock::new(HashMap::new())), } } } impl Repository for InMemoryRepository { async fn create(&self, data: T::Create) -> Result { let entity = T::from_create_data(data)?; let id = entity.id(); self.entities.write().await.insert(id, entity.clone()); Ok(entity) } async fn find_by_id(&self, id: Uuid) -> Result { self.entities .read() .await .get(&id) .cloned() .ok_or_else(|| domain::DomainError::NotFound(format!("{} not found: {}", T::entity_name(), id))) } async fn find_all(&self) -> Result> { Ok(self.entities.read().await.values().cloned().collect()) } async fn update(&self, id: Uuid, data: T::Update) -> Result { let mut entities = self.entities.write().await; let entity = entities .get_mut(&id) .ok_or_else(|| domain::DomainError::NotFound(format!("{} not found: {}", T::entity_name(), id)))?; entity.update(data)?; Ok(entity.clone()) } async fn delete(&self, id: Uuid) -> Result<()> { self.entities .write() .await .remove(&id) .map(|_| ()) .ok_or_else(|| domain::DomainError::NotFound(format!("{} not found: {}", T::entity_name(), id))) } } // Implement MemoryEntity for User impl MemoryEntity for User { /// Get the entity's ID fn id(&self) -> Uuid { self.id() } /// Create a new entity from create data fn from_create_data(data: CreateUser) -> Result { User::new( Uuid::new_v4(), data.username().to_string(), data.email().to_string() ) } /// Update the entity with update data fn update(&mut self, data: UpdateUser) -> Result<()> { if let Some(username) = data.username() { self.set_username(username.to_string())?; } if let Some(email) = data.email() { self.set_email(email.to_string())?; } Ok(()) } /// Get the entity name for error messages fn entity_name() -> &'static str { "User" } } // Implement MemoryEntity for Product impl MemoryEntity for Product { /// Get the entity's ID fn id(&self) -> Uuid { self.id() } /// Create a new entity from create data fn from_create_data(data: CreateProduct) -> Result { Product::new( Uuid::new_v4(), data.name().to_string(), data.description().to_string() ) } /// Update the entity with update data fn update(&mut self, data: UpdateProduct) -> Result<()> { if let Some(name) = data.name() { self.set_name(name.to_string())?; } if let Some(description) = data.description() { self.set_description(description.to_string())?; } Ok(()) } /// Get the entity name for error messages fn entity_name() -> &'static str { "Product" } } /// Thread-safe, async in-memory repository for `User` entities. /// /// Implements all CRUD operations. Intended for testing and non-persistent use cases. pub type InMemoryUserRepository = InMemoryRepository; /// Thread-safe, async in-memory repository for `Product` entities. /// /// Implements all CRUD operations. Intended for testing and non-persistent use cases. pub type InMemoryProductRepository = InMemoryRepository; /// Type alias for a user service backed by the in-memory repository. /// /// Provides a high-level API for user operations using `InMemoryUserRepository`. pub type MemoryUserService = Service; /// Type alias for a product service backed by the in-memory repository. /// /// Provides a high-level API for product operations using `InMemoryProductRepository`. pub type MemoryProductService = Service; #[cfg(test)] mod tests { use super::*; use domain::{CreateUser, UpdateUser, CreateProduct, UpdateProduct, DomainError}; mod user_repository_tests { use super::*; #[tokio::test] async fn test_create_user() { let repo = InMemoryUserRepository::new(); let create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); let user = repo.create(create_data).await.unwrap(); assert_eq!(user.username(), "testuser"); assert_eq!(user.email(), "test@example.com"); assert!(!user.id().is_nil()); assert!(user.created_at() <= chrono::Utc::now()); } #[tokio::test] async fn test_find_by_id_existing() { let repo = InMemoryUserRepository::new(); let create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); let created_user = repo.create(create_data).await.unwrap(); let found_user = repo.find_by_id(created_user.id()).await.unwrap(); assert_eq!(found_user.id(), created_user.id()); assert_eq!(found_user.username(), created_user.username()); assert_eq!(found_user.email(), created_user.email()); } #[tokio::test] async fn test_find_by_id_not_found() { let repo = InMemoryUserRepository::new(); let non_existent_id = Uuid::new_v4(); let result = repo.find_by_id(non_existent_id).await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[tokio::test] async fn test_find_all_empty() { let repo = InMemoryUserRepository::new(); let users = repo.find_all().await.unwrap(); assert!(users.is_empty()); } #[tokio::test] async fn test_find_all_with_users() { let repo = InMemoryUserRepository::new(); let create_data1 = CreateUser::new("user1".to_string(), "user1@example.com".to_string()).unwrap(); let create_data2 = CreateUser::new("user2".to_string(), "user2@example.com".to_string()).unwrap(); repo.create(create_data1).await.unwrap(); repo.create(create_data2).await.unwrap(); let users = repo.find_all().await.unwrap(); assert_eq!(users.len(), 2); } #[tokio::test] async fn test_update_user_existing() { let repo = InMemoryUserRepository::new(); let create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); let created_user = repo.create(create_data).await.unwrap(); let update_data = UpdateUser::new(Some("updateduser".to_string()), Some("updated@example.com".to_string())).unwrap(); let updated_user = repo.update(created_user.id(), update_data).await.unwrap(); assert_eq!(updated_user.username(), "updateduser"); assert_eq!(updated_user.email(), "updated@example.com"); } #[tokio::test] async fn test_update_user_partial() { let repo = InMemoryUserRepository::new(); let create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); let created_user = repo.create(create_data).await.unwrap(); let update_data = UpdateUser::new(Some("updateduser".to_string()), None).unwrap(); let updated_user = repo.update(created_user.id(), update_data).await.unwrap(); assert_eq!(updated_user.username(), "updateduser"); assert_eq!(updated_user.email(), "test@example.com"); // Unchanged } #[tokio::test] async fn test_update_user_not_found() { let repo = InMemoryUserRepository::new(); let non_existent_id = Uuid::new_v4(); let update_data = UpdateUser::new(Some("updateduser".to_string()), None).unwrap(); let result = repo.update(non_existent_id, update_data).await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[tokio::test] async fn test_delete_user_existing() { let repo = InMemoryUserRepository::new(); let create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); let created_user = repo.create(create_data).await.unwrap(); let result = repo.delete(created_user.id()).await; assert!(result.is_ok()); assert!(repo.find_all().await.unwrap().is_empty()); } #[tokio::test] async fn test_delete_user_not_found() { let repo = InMemoryUserRepository::new(); let non_existent_id = Uuid::new_v4(); let result = repo.delete(non_existent_id).await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[tokio::test] async fn test_concurrent_access() { let repo = InMemoryUserRepository::new(); let repo_clone = repo.clone(); // Spawn multiple tasks that create users concurrently let handles: Vec<_> = (0..10) .map(|i| { let repo = repo_clone.clone(); tokio::spawn(async move { let create_data = CreateUser::new( format!("user{}", i), format!("user{}@example.com", i) ).unwrap(); repo.create(create_data).await }) }) .collect(); // Wait for all tasks to complete for handle in handles { handle.await.unwrap().unwrap(); } let users = repo.find_all().await.unwrap(); assert_eq!(users.len(), 10); } } mod product_repository_tests { use super::*; #[tokio::test] async fn test_create_product() { let repo = InMemoryProductRepository::new(); let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); let product = repo.create(create_data).await.unwrap(); assert_eq!(product.name(), "testproduct"); assert_eq!(product.description(), "Test description"); assert!(!product.id().is_nil()); assert!(product.created_at() <= chrono::Utc::now()); } #[tokio::test] async fn test_find_by_id_existing() { let repo = InMemoryProductRepository::new(); let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); let created_product = repo.create(create_data).await.unwrap(); let found_product = repo.find_by_id(created_product.id()).await.unwrap(); assert_eq!(found_product.id(), created_product.id()); assert_eq!(found_product.name(), created_product.name()); assert_eq!(found_product.description(), created_product.description()); } #[tokio::test] async fn test_find_by_id_not_found() { let repo = InMemoryProductRepository::new(); let non_existent_id = Uuid::new_v4(); let result = repo.find_by_id(non_existent_id).await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[tokio::test] async fn test_find_all_empty() { let repo = InMemoryProductRepository::new(); let products = repo.find_all().await.unwrap(); assert!(products.is_empty()); } #[tokio::test] async fn test_find_all_with_products() { let repo = InMemoryProductRepository::new(); let create_data1 = CreateProduct::new("product1".to_string(), "Description 1".to_string()).unwrap(); let create_data2 = CreateProduct::new("product2".to_string(), "Description 2".to_string()).unwrap(); repo.create(create_data1).await.unwrap(); repo.create(create_data2).await.unwrap(); let products = repo.find_all().await.unwrap(); assert_eq!(products.len(), 2); } #[tokio::test] async fn test_update_product_existing() { let repo = InMemoryProductRepository::new(); let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); let created_product = repo.create(create_data).await.unwrap(); let update_data = UpdateProduct::new(Some("updatedproduct".to_string()), Some("Updated description".to_string())).unwrap(); let updated_product = repo.update(created_product.id(), update_data).await.unwrap(); assert_eq!(updated_product.name(), "updatedproduct"); assert_eq!(updated_product.description(), "Updated description"); } #[tokio::test] async fn test_update_product_partial() { let repo = InMemoryProductRepository::new(); let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); let created_product = repo.create(create_data).await.unwrap(); let update_data = UpdateProduct::new(Some("updatedproduct".to_string()), None).unwrap(); let updated_product = repo.update(created_product.id(), update_data).await.unwrap(); assert_eq!(updated_product.name(), "updatedproduct"); assert_eq!(updated_product.description(), "Test description"); // Unchanged } #[tokio::test] async fn test_update_product_not_found() { let repo = InMemoryProductRepository::new(); let non_existent_id = Uuid::new_v4(); let update_data = UpdateProduct::new(Some("updatedproduct".to_string()), None).unwrap(); let result = repo.update(non_existent_id, update_data).await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[tokio::test] async fn test_delete_product_existing() { let repo = InMemoryProductRepository::new(); let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); let created_product = repo.create(create_data).await.unwrap(); let result = repo.delete(created_product.id()).await; assert!(result.is_ok()); assert!(repo.find_all().await.unwrap().is_empty()); } #[tokio::test] async fn test_delete_product_not_found() { let repo = InMemoryProductRepository::new(); let non_existent_id = Uuid::new_v4(); let result = repo.delete(non_existent_id).await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[tokio::test] async fn test_concurrent_access() { let repo = InMemoryProductRepository::new(); let repo_clone = repo.clone(); // Spawn multiple tasks that create products concurrently let handles: Vec<_> = (0..10) .map(|i| { let repo = repo_clone.clone(); tokio::spawn(async move { let create_data = CreateProduct::new( format!("product{}", i), format!("Description {}", i) ).unwrap(); repo.create(create_data).await }) }) .collect(); // Wait for all tasks to complete for handle in handles { handle.await.unwrap().unwrap(); } let products = repo.find_all().await.unwrap(); assert_eq!(products.len(), 10); } } mod service_tests { use super::*; use application::UseCase; #[tokio::test] async fn test_memory_user_service() { let service = MemoryUserService::new(InMemoryUserRepository::new()); let create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); let user = service.create(create_data).await.unwrap(); assert_eq!(user.username(), "testuser"); let found_user = service.get(user.id()).await.unwrap(); assert_eq!(found_user.id(), user.id()); let users = service.list().await.unwrap(); assert_eq!(users.len(), 1); let update_data = UpdateUser::new(Some("updateduser".to_string()), None).unwrap(); let updated_user = service.update(user.id(), update_data).await.unwrap(); assert_eq!(updated_user.username(), "updateduser"); service.delete(user.id()).await.unwrap(); let users_after_delete = service.list().await.unwrap(); assert!(users_after_delete.is_empty()); } #[tokio::test] async fn test_memory_product_service() { let service = MemoryProductService::new(InMemoryProductRepository::new()); let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); let product = service.create(create_data).await.unwrap(); assert_eq!(product.name(), "testproduct"); let found_product = service.get(product.id()).await.unwrap(); assert_eq!(found_product.id(), product.id()); let products = service.list().await.unwrap(); assert_eq!(products.len(), 1); let update_data = UpdateProduct::new(Some("updatedproduct".to_string()), None).unwrap(); let updated_product = service.update(product.id(), update_data).await.unwrap(); assert_eq!(updated_product.name(), "updatedproduct"); service.delete(product.id()).await.unwrap(); let products_after_delete = service.list().await.unwrap(); assert!(products_after_delete.is_empty()); } } mod integration_tests { use super::*; #[tokio::test] async fn test_user_lifecycle() { let repo = InMemoryUserRepository::new(); // Create let create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); let user = repo.create(create_data).await.unwrap(); assert_eq!(user.username(), "testuser"); // Read let found_user = repo.find_by_id(user.id()).await.unwrap(); assert_eq!(found_user.id(), user.id()); // Update let update_data = UpdateUser::new(Some("updateduser".to_string()), Some("updated@example.com".to_string())).unwrap(); let updated_user = repo.update(user.id(), update_data).await.unwrap(); assert_eq!(updated_user.username(), "updateduser"); assert_eq!(updated_user.email(), "updated@example.com"); // Delete repo.delete(user.id()).await.unwrap(); let result = repo.find_by_id(user.id()).await; assert!(result.is_err()); } #[tokio::test] async fn test_product_lifecycle() { let repo = InMemoryProductRepository::new(); // Create let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); let product = repo.create(create_data).await.unwrap(); assert_eq!(product.name(), "testproduct"); // Read let found_product = repo.find_by_id(product.id()).await.unwrap(); assert_eq!(found_product.id(), product.id()); // Update let update_data = UpdateProduct::new(Some("updatedproduct".to_string()), Some("Updated description".to_string())).unwrap(); let updated_product = repo.update(product.id(), update_data).await.unwrap(); assert_eq!(updated_product.name(), "updatedproduct"); assert_eq!(updated_product.description(), "Updated description"); // Delete repo.delete(product.id()).await.unwrap(); let result = repo.find_by_id(product.id()).await; assert!(result.is_err()); } } }