/* * 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 chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; use std::future::Future; #[derive(Debug, Error)] pub enum DomainError { #[error("Entity not found: {0}")] NotFound(String), #[error("Invalid input: {0}")] InvalidInput(String), #[error("Internal error: {0}")] Internal(String), } unsafe impl Send for DomainError {} unsafe impl Sync for DomainError {} pub type Result = std::result::Result; pub trait Entity: Send + Sync { type Create: Send; type Update: Send; } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { id: Uuid, username: String, email: String, created_at: DateTime, updated_at: DateTime, } impl User { // Constructor with validation pub fn new(id: Uuid, username: String, email: String) -> Result { if username.trim().is_empty() { return Err(DomainError::InvalidInput("Username cannot be empty".to_string())); } let now = Utc::now(); Ok(Self { id, username, email, created_at: now, updated_at: now, }) } // Constructor for database reconstruction (bypasses validation) pub fn from_db(id: Uuid, username: String, email: String, created_at: DateTime, updated_at: DateTime) -> Self { Self { id, username, email, created_at, updated_at, } } // Getters pub fn id(&self) -> Uuid { self.id } pub fn username(&self) -> &str { &self.username } pub fn email(&self) -> &str { &self.email } pub fn created_at(&self) -> DateTime { self.created_at } pub fn updated_at(&self) -> DateTime { self.updated_at } // Setters with validation pub fn set_username(&mut self, username: String) -> Result<()> { if username.trim().is_empty() { return Err(DomainError::InvalidInput("Username cannot be empty".to_string())); } self.username = username; self.updated_at = Utc::now(); Ok(()) } pub fn set_email(&mut self, email: String) -> Result<()> { self.email = email; self.updated_at = Utc::now(); Ok(()) } // Method to update from UpdateUser pub fn update(&mut self, update: UpdateUser) -> Result<()> { if let Some(username) = update.username() { self.set_username(username.to_string())?; } if let Some(email) = update.email() { self.set_email(email.to_string())?; } Ok(()) } } impl Entity for User { type Create = CreateUser; type Update = UpdateUser; } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Product { id: Uuid, name: String, description: String, created_at: DateTime, updated_at: DateTime, } impl Product { // Constructor with validation pub fn new(id: Uuid, name: String, description: String) -> Result { if name.trim().is_empty() { return Err(DomainError::InvalidInput("Product name cannot be empty".to_string())); } let now = Utc::now(); Ok(Self { id, name, description, created_at: now, updated_at: now, }) } // Constructor for database reconstruction (bypasses validation) pub fn from_db(id: Uuid, name: String, description: String, created_at: DateTime, updated_at: DateTime) -> Self { Self { id, name, description, created_at, updated_at, } } // Getters pub fn id(&self) -> Uuid { self.id } pub fn name(&self) -> &str { &self.name } pub fn description(&self) -> &str { &self.description } pub fn created_at(&self) -> DateTime { self.created_at } pub fn updated_at(&self) -> DateTime { self.updated_at } // Setters with validation pub fn set_name(&mut self, name: String) -> Result<()> { if name.trim().is_empty() { return Err(DomainError::InvalidInput("Product name cannot be empty".to_string())); } self.name = name; self.updated_at = Utc::now(); Ok(()) } pub fn set_description(&mut self, description: String) -> Result<()> { self.description = description; self.updated_at = Utc::now(); Ok(()) } // Method to update from UpdateProduct pub fn update(&mut self, update: UpdateProduct) -> Result<()> { if let Some(name) = update.name() { self.set_name(name.to_string())?; } if let Some(description) = update.description() { self.set_description(description.to_string())?; } Ok(()) } } impl Entity for Product { type Create = CreateProduct; type Update = UpdateProduct; } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateUser { username: String, email: String, } impl CreateUser { pub fn new(username: String, email: String) -> Result { if username.trim().is_empty() { return Err(DomainError::InvalidInput("Username cannot be empty".to_string())); } Ok(Self { username, email }) } // Getters pub fn username(&self) -> &str { &self.username } pub fn email(&self) -> &str { &self.email } // Consuming getters for when you need to take ownership pub fn into_username(self) -> String { self.username } pub fn into_email(self) -> String { self.email } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateUser { username: Option, email: Option, } impl UpdateUser { pub fn new(username: Option, email: Option) -> Result { if let Some(ref username) = username { if username.trim().is_empty() { return Err(DomainError::InvalidInput("Username cannot be empty".to_string())); } } Ok(Self { username, email }) } // Getters pub fn username(&self) -> Option<&str> { self.username.as_deref() } pub fn email(&self) -> Option<&str> { self.email.as_deref() } // Setters with validation pub fn set_username(&mut self, username: Option) -> Result<()> { if let Some(ref username) = username { if username.trim().is_empty() { return Err(DomainError::InvalidInput("Username cannot be empty".to_string())); } } self.username = username; Ok(()) } pub fn set_email(&mut self, email: Option) -> Result<()> { self.email = email; Ok(()) } // Builder pattern methods pub fn with_username(mut self, username: Option) -> Result { self.set_username(username)?; Ok(self) } pub fn with_email(mut self, email: Option) -> Result { self.set_email(email)?; Ok(self) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateProduct { name: String, description: String, } impl CreateProduct { pub fn new(name: String, description: String) -> Result { if name.trim().is_empty() { return Err(DomainError::InvalidInput("Product name cannot be empty".to_string())); } Ok(Self { name, description }) } // Getters pub fn name(&self) -> &str { &self.name } pub fn description(&self) -> &str { &self.description } // Consuming getters for when you need to take ownership pub fn into_name(self) -> String { self.name } pub fn into_description(self) -> String { self.description } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateProduct { name: Option, description: Option, } impl UpdateProduct { pub fn new(name: Option, description: Option) -> Result { if let Some(ref name) = name { if name.trim().is_empty() { return Err(DomainError::InvalidInput("Product name cannot be empty".to_string())); } } Ok(Self { name, description }) } // Getters pub fn name(&self) -> Option<&str> { self.name.as_deref() } pub fn description(&self) -> Option<&str> { self.description.as_deref() } // Setters with validation pub fn set_name(&mut self, name: Option) -> Result<()> { if let Some(ref name) = name { if name.trim().is_empty() { return Err(DomainError::InvalidInput("Product name cannot be empty".to_string())); } } self.name = name; Ok(()) } pub fn set_description(&mut self, description: Option) -> Result<()> { self.description = description; Ok(()) } // Builder pattern methods pub fn with_name(mut self, name: Option) -> Result { self.set_name(name)?; Ok(self) } pub fn with_description(mut self, description: Option) -> Result { self.set_description(description)?; Ok(self) } } pub trait Repository: Send + Sync where T: Send, Create: Send, Update: Send, { fn create(&self, data: Create) -> impl Future> + Send; fn find_by_id(&self, id: Uuid) -> impl Future> + Send; fn find_all(&self) -> impl Future>> + Send; fn update(&self, id: Uuid, data: Update) -> impl Future> + Send; fn delete(&self, id: Uuid) -> impl Future> + Send; } #[cfg(test)] mod tests { use super::*; use chrono::Utc; mod user_tests { use super::*; #[test] fn test_user_entity_impl() { let user = User::new( Uuid::new_v4(), "test_user".to_string(), "test@example.com".to_string() ).unwrap(); assert_eq!(user.username(), "test_user"); assert_eq!(user.email(), "test@example.com"); } #[test] fn test_user_from_db() { let id = Uuid::new_v4(); let created_at = Utc::now(); let updated_at = Utc::now(); let user = User::from_db( id, "test_user".to_string(), "test@example.com".to_string(), created_at, updated_at ); assert_eq!(user.id(), id); assert_eq!(user.username(), "test_user"); assert_eq!(user.email(), "test@example.com"); assert_eq!(user.created_at(), created_at); assert_eq!(user.updated_at(), updated_at); } #[test] fn test_user_setters() { let mut user = User::new( Uuid::new_v4(), "test_user".to_string(), "test@example.com".to_string() ).unwrap(); // Test valid updates user.set_username("new_username".to_string()).unwrap(); user.set_email("new@example.com".to_string()).unwrap(); assert_eq!(user.username(), "new_username"); assert_eq!(user.email(), "new@example.com"); } #[test] fn test_user_setter_validation() { let mut user = User::new( Uuid::new_v4(), "test_user".to_string(), "test@example.com".to_string() ).unwrap(); // Test invalid username let result = user.set_username("".to_string()); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_))); // Test empty email (should be allowed) let result = user.set_email("".to_string()); assert!(result.is_ok()); assert_eq!(user.email(), ""); } #[test] fn test_user_update_method() { let mut user = User::new( Uuid::new_v4(), "test_user".to_string(), "test@example.com".to_string() ).unwrap(); let update = UpdateUser::new( Some("new_username".to_string()), Some("new@example.com".to_string()) ).unwrap(); user.update(update).unwrap(); assert_eq!(user.username(), "new_username"); assert_eq!(user.email(), "new@example.com"); } #[test] fn test_create_user_validation() { let create_user = CreateUser::new("test_user".to_string(), "test@example.com".to_string()).unwrap(); assert_eq!(create_user.username(), "test_user"); assert_eq!(create_user.email(), "test@example.com"); } #[test] fn test_create_user_empty_username() { let result = CreateUser::new("".to_string(), "test@example.com".to_string()); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_))); } #[test] fn test_create_user_whitespace_username() { let result = CreateUser::new(" ".to_string(), "test@example.com".to_string()); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_))); } #[test] fn test_create_user_empty_email() { let result = CreateUser::new("test_user".to_string(), "".to_string()); assert!(result.is_ok()); let create_user = result.unwrap(); assert_eq!(create_user.email(), ""); } #[test] fn test_create_user_whitespace_email() { let result = CreateUser::new("test_user".to_string(), " ".to_string()); assert!(result.is_ok()); let create_user = result.unwrap(); assert_eq!(create_user.email(), " "); } #[test] fn test_update_user_partial() { let update_user = UpdateUser::new(Some("new_username".to_string()), None).unwrap(); assert_eq!(update_user.username(), Some("new_username")); assert_eq!(update_user.email(), None); } #[test] fn test_update_user_empty_username() { let result = UpdateUser::new(Some("".to_string()), None); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_))); } #[test] fn test_update_user_whitespace_username() { let result = UpdateUser::new(Some(" ".to_string()), None); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_))); } #[test] fn test_update_user_empty_email() { let result = UpdateUser::new(None, Some("".to_string())); assert!(result.is_ok()); let update_user = result.unwrap(); assert_eq!(update_user.email(), Some("")); } #[test] fn test_update_user_whitespace_email() { let result = UpdateUser::new(None, Some(" ".to_string())); assert!(result.is_ok()); let update_user = result.unwrap(); assert_eq!(update_user.email(), Some(" ")); } #[test] fn test_update_user_setters() { let mut update_user = UpdateUser::new(None, None).unwrap(); // Test valid updates update_user.set_username(Some("new_username".to_string())).unwrap(); update_user.set_email(Some("new@example.com".to_string())).unwrap(); assert_eq!(update_user.username(), Some("new_username")); assert_eq!(update_user.email(), Some("new@example.com")); } #[test] fn test_update_user_builder_pattern() { let update_user = UpdateUser::new(None, None) .unwrap() .with_username(Some("new_username".to_string())) .unwrap() .with_email(Some("new@example.com".to_string())) .unwrap(); assert_eq!(update_user.username(), Some("new_username")); assert_eq!(update_user.email(), Some("new@example.com")); } } mod product_tests { use super::*; #[test] fn test_product_entity_impl() { let product = Product::new( Uuid::new_v4(), "Test Product".to_string(), "Test Description".to_string() ).unwrap(); assert_eq!(product.name(), "Test Product"); assert_eq!(product.description(), "Test Description"); } #[test] fn test_product_from_db() { let id = Uuid::new_v4(); let created_at = Utc::now(); let updated_at = Utc::now(); let product = Product::from_db( id, "Test Product".to_string(), "Test Description".to_string(), created_at, updated_at ); assert_eq!(product.id(), id); assert_eq!(product.name(), "Test Product"); assert_eq!(product.description(), "Test Description"); assert_eq!(product.created_at(), created_at); assert_eq!(product.updated_at(), updated_at); } #[test] fn test_product_setters() { let mut product = Product::new( Uuid::new_v4(), "Test Product".to_string(), "Test Description".to_string() ).unwrap(); // Test valid updates product.set_name("New Product Name".to_string()).unwrap(); product.set_description("New Description".to_string()).unwrap(); assert_eq!(product.name(), "New Product Name"); assert_eq!(product.description(), "New Description"); } #[test] fn test_product_setter_validation() { let mut product = Product::new( Uuid::new_v4(), "Test Product".to_string(), "Test Description".to_string() ).unwrap(); // Test invalid name let result = product.set_name("".to_string()); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_))); } #[test] fn test_product_update_method() { let mut product = Product::new( Uuid::new_v4(), "Test Product".to_string(), "Test Description".to_string() ).unwrap(); let update = UpdateProduct::new( Some("New Product Name".to_string()), Some("New Description".to_string()) ).unwrap(); product.update(update).unwrap(); assert_eq!(product.name(), "New Product Name"); assert_eq!(product.description(), "New Description"); } #[test] fn test_create_product_validation() { let create_product = CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap(); assert_eq!(create_product.name(), "Test Product"); assert_eq!(create_product.description(), "Test Description"); } #[test] fn test_create_product_empty_name() { let result = CreateProduct::new("".to_string(), "desc".to_string()); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_))); } #[test] fn test_create_product_whitespace_name() { let result = CreateProduct::new(" ".to_string(), "desc".to_string()); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_))); } #[test] fn test_create_product_empty_description() { let create_product = CreateProduct::new("Test Product".to_string(), "".to_string()).unwrap(); assert_eq!(create_product.name(), "Test Product"); assert_eq!(create_product.description(), ""); } #[test] fn test_update_product_partial() { let update_product = UpdateProduct::new(Some("New Product Name".to_string()), None).unwrap(); assert_eq!(update_product.name(), Some("New Product Name")); assert_eq!(update_product.description(), None); } #[test] fn test_update_product_empty_name() { let result = UpdateProduct::new(Some("".to_string()), None); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_))); } #[test] fn test_update_product_whitespace_name() { let result = UpdateProduct::new(Some(" ".to_string()), None); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_))); } #[test] fn test_update_product_empty_description() { let update_product = UpdateProduct::new(None, Some("".to_string())).unwrap(); assert_eq!(update_product.name(), None); assert_eq!(update_product.description(), Some("")); } #[test] fn test_update_product_setters() { let mut update_product = UpdateProduct::new(None, None).unwrap(); // Test valid updates update_product.set_name(Some("New Product Name".to_string())).unwrap(); update_product.set_description(Some("New Description".to_string())).unwrap(); assert_eq!(update_product.name(), Some("New Product Name")); assert_eq!(update_product.description(), Some("New Description")); } #[test] fn test_update_product_builder_pattern() { let update_product = UpdateProduct::new(None, None) .unwrap() .with_name(Some("New Product Name".to_string())) .unwrap() .with_description(Some("New Description".to_string())) .unwrap(); assert_eq!(update_product.name(), Some("New Product Name")); assert_eq!(update_product.description(), Some("New Description")); } } mod domain_error_tests { use super::*; #[test] fn test_not_found_error() { let error = DomainError::NotFound("User not found".to_string()); assert_eq!(error.to_string(), "Entity not found: User not found"); } #[test] fn test_invalid_input_error() { let error = DomainError::InvalidInput("Invalid email format".to_string()); assert_eq!(error.to_string(), "Invalid input: Invalid email format"); } #[test] fn test_internal_error() { let error = DomainError::Internal("Database connection failed".to_string()); assert_eq!(error.to_string(), "Internal error: Database connection failed"); } } mod repository_trait_tests { use super::*; use std::sync::Arc; use tokio::sync::RwLock; use std::collections::HashMap; // Mock implementation of Repository for testing struct MockRepository { data: Arc>>, } impl MockRepository { fn new() -> Self { Self { data: Arc::new(RwLock::new(HashMap::new())), } } } #[tokio::test] async fn test_repository_create() { let repo = MockRepository::::new(); let create_product = CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap(); let product = Product::new( Uuid::new_v4(), create_product.name().to_string(), create_product.description().to_string() ).unwrap(); repo.data.write().await.insert(product.id(), product.clone()); let guard = repo.data.read().await; let stored = guard.get(&product.id()).unwrap(); assert_eq!(stored.name(), "Test Product"); assert_eq!(stored.description(), "Test Description"); } #[tokio::test] async fn test_repository_find_by_id() { let repo = MockRepository::::new(); let product = Product::new( Uuid::new_v4(), "Test Product".to_string(), "Test Description".to_string() ).unwrap(); repo.data.write().await.insert(product.id(), product.clone()); let guard = repo.data.read().await; let found = guard.get(&product.id()).unwrap(); assert_eq!(found.id(), product.id()); } #[tokio::test] async fn test_repository_find_all() { let repo = MockRepository::::new(); let product1 = Product::new( Uuid::new_v4(), "Product 1".to_string(), "Description 1".to_string() ).unwrap(); let product2 = Product::new( Uuid::new_v4(), "Product 2".to_string(), "Description 2".to_string() ).unwrap(); repo.data.write().await.insert(product1.id(), product1.clone()); repo.data.write().await.insert(product2.id(), product2.clone()); let all = repo.data.read().await; assert_eq!(all.len(), 2); } #[tokio::test] async fn test_repository_update() { let repo = MockRepository::::new(); let product = Product::new( Uuid::new_v4(), "Test Product".to_string(), "Test Description".to_string() ).unwrap(); repo.data.write().await.insert(product.id(), product.clone()); let mut guard = repo.data.write().await; let stored = guard.get_mut(&product.id()).unwrap(); stored.set_name("Updated Product".to_string()).unwrap(); drop(guard); let read_guard = repo.data.read().await; let updated = read_guard.get(&product.id()).unwrap(); assert_eq!(updated.name(), "Updated Product"); } #[tokio::test] async fn test_repository_delete() { let repo = MockRepository::::new(); let product = Product::new( Uuid::new_v4(), "Test Product".to_string(), "Test Description".to_string() ).unwrap(); repo.data.write().await.insert(product.id(), product.clone()); repo.data.write().await.remove(&product.id()); assert!(repo.data.read().await.get(&product.id()).is_none()); } } }