/* * 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 domain::{Result, Entity}; use thiserror::Error; use uuid::Uuid; use std::marker::PhantomData; use std::future::Future; #[derive(Debug, Error)] pub enum ApplicationError { #[error("Domain error: {0}")] Domain(#[from] domain::DomainError), #[error("Repository error: {0}")] Repository(String), } pub type AppResult = std::result::Result; pub trait Repository: Send + Sync { fn create(&self, data: T::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: T::Update) -> impl Future> + Send; fn delete(&self, id: Uuid) -> impl Future> + Send; } pub trait UseCase { fn create(&self, data: T::Create) -> impl Future> + Send; fn get(&self, id: Uuid) -> impl Future> + Send; fn list(&self) -> impl Future>> + Send; fn update(&self, id: Uuid, data: T::Update) -> impl Future> + Send; fn delete(&self, id: Uuid) -> impl Future> + Send; } #[derive(Clone)] pub struct Service + Clone> { repository: R, _phantom: PhantomData, } impl + Clone> Service { pub fn new(repository: R) -> Self { Self { repository, _phantom: PhantomData, } } } impl + Clone> UseCase for Service { fn create(&self, data: T::Create) -> impl Future> + Send { async move { self.repository.create(data).await.map_err(ApplicationError::Domain) } } fn get(&self, id: Uuid) -> impl Future> + Send { async move { self.repository.find_by_id(id).await.map_err(ApplicationError::Domain) } } fn list(&self) -> impl Future>> + Send { async move { self.repository.find_all().await.map_err(ApplicationError::Domain) } } fn update(&self, id: Uuid, data: T::Update) -> impl Future> + Send { async move { self.repository.update(id, data).await.map_err(ApplicationError::Domain) } } fn delete(&self, id: Uuid) -> impl Future> + Send { async move { self.repository.delete(id).await.map_err(ApplicationError::Domain) } } } #[cfg(test)] mod tests { use super::*; use domain::{User, CreateUser, UpdateUser, Product, CreateProduct, UpdateProduct}; use std::sync::Arc; use tokio::sync::RwLock; use std::collections::HashMap; // Mock repository for testing #[derive(Clone)] struct MockRepository { data: Arc>>, } impl MockRepository { fn new() -> Self { Self { data: Arc::new(RwLock::new(HashMap::new())), } } } impl Repository for MockRepository { fn create(&self, data: CreateUser) -> impl Future> + Send { async move { let mut guard = self.data.write().await; let id = Uuid::new_v4(); let user = User::new(id, data.username().to_string(), data.email().to_string())?; guard.insert(id, user.clone()); Ok(user) } } fn find_by_id(&self, id: Uuid) -> impl Future> + Send { async move { let guard = self.data.read().await; guard.get(&id) .cloned() .ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id))) } } fn find_all(&self) -> impl Future>> + Send { async move { let guard = self.data.read().await; Ok(guard.values().cloned().collect()) } } fn update(&self, id: Uuid, data: UpdateUser) -> impl Future> + Send { async move { let mut guard = self.data.write().await; let user = guard.get_mut(&id) .ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?; if let Some(username) = data.username() { user.set_username(username.to_string())?; } if let Some(email) = data.email() { user.set_email(email.to_string())?; } Ok(user.clone()) } } fn delete(&self, id: Uuid) -> impl Future> + Send { async move { let mut guard = self.data.write().await; guard.remove(&id) .ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?; Ok(()) } } } impl Repository for MockRepository { fn create(&self, data: CreateProduct) -> impl Future> + Send { async move { let mut guard = self.data.write().await; let id = Uuid::new_v4(); let product = Product::new(id, data.name().to_string(), data.description().to_string())?; guard.insert(id, product.clone()); Ok(product) } } fn find_by_id(&self, id: Uuid) -> impl Future> + Send { async move { let guard = self.data.read().await; guard.get(&id) .cloned() .ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id))) } } fn find_all(&self) -> impl Future>> + Send { async move { let guard = self.data.read().await; Ok(guard.values().cloned().collect()) } } fn update(&self, id: Uuid, data: UpdateProduct) -> impl Future> + Send { async move { let mut guard = self.data.write().await; let product = guard.get_mut(&id) .ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?; if let Some(name) = data.name() { product.set_name(name.to_string())?; } if let Some(description) = data.description() { product.set_description(description.to_string())?; } Ok(product.clone()) } } fn delete(&self, id: Uuid) -> impl Future> + Send { async move { let mut guard = self.data.write().await; guard.remove(&id) .ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?; Ok(()) } } } mod service_tests { use super::*; #[tokio::test] async fn test_user_service_create() { let repo = MockRepository::::new(); let service = Service::new(repo); let create_user = CreateUser::new("test_user".to_string(), "test@example.com".to_string()).unwrap(); let user = service.create(create_user).await.unwrap(); assert_eq!(user.username(), "test_user"); assert_eq!(user.email(), "test@example.com"); } #[tokio::test] async fn test_user_service_get() { let repo = MockRepository::::new(); let service = Service::new(repo); let create_user = CreateUser::new("test_user".to_string(), "test@example.com".to_string()).unwrap(); let created = service.create(create_user).await.unwrap(); let found = service.get(created.id()).await.unwrap(); assert_eq!(found.id(), created.id()); } #[tokio::test] async fn test_user_service_list() { let repo = MockRepository::::new(); let service = Service::new(repo); let user1 = CreateUser::new("user1".to_string(), "user1@example.com".to_string()).unwrap(); let user2 = CreateUser::new("user2".to_string(), "user2@example.com".to_string()).unwrap(); service.create(user1).await.unwrap(); service.create(user2).await.unwrap(); let users = service.list().await.unwrap(); assert_eq!(users.len(), 2); } #[tokio::test] async fn test_user_service_update() { let repo = MockRepository::::new(); let service = Service::new(repo); let create_user = CreateUser::new("test_user".to_string(), "test@example.com".to_string()).unwrap(); let created = service.create(create_user).await.unwrap(); let update = UpdateUser::new(Some("updated_user".to_string()), None).unwrap(); let updated = service.update(created.id(), update).await.unwrap(); assert_eq!(updated.username(), "updated_user"); assert_eq!(updated.email(), "test@example.com"); } #[tokio::test] async fn test_user_service_delete() { let repo = MockRepository::::new(); let service = Service::new(repo); let create_user = CreateUser::new("test_user".to_string(), "test@example.com".to_string()).unwrap(); let created = service.create(create_user).await.unwrap(); service.delete(created.id()).await.unwrap(); assert!(service.get(created.id()).await.is_err()); } #[tokio::test] async fn test_product_service_create() { let repo = MockRepository::::new(); let service = Service::new(repo); let create_product = CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap(); let product = service.create(create_product).await.unwrap(); assert_eq!(product.name(), "Test Product"); assert_eq!(product.description(), "Test Description"); } #[tokio::test] async fn test_product_service_get() { let repo = MockRepository::::new(); let service = Service::new(repo); let create_product = CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap(); let created = service.create(create_product).await.unwrap(); let found = service.get(created.id()).await.unwrap(); assert_eq!(found.id(), created.id()); } #[tokio::test] async fn test_product_service_list() { let repo = MockRepository::::new(); let service = Service::new(repo); let product1 = CreateProduct::new("Product 1".to_string(), "Description 1".to_string()).unwrap(); let product2 = CreateProduct::new("Product 2".to_string(), "Description 2".to_string()).unwrap(); service.create(product1).await.unwrap(); service.create(product2).await.unwrap(); let products = service.list().await.unwrap(); assert_eq!(products.len(), 2); } #[tokio::test] async fn test_product_service_update() { let repo = MockRepository::::new(); let service = Service::new(repo); let create_product = CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap(); let created = service.create(create_product).await.unwrap(); let update = UpdateProduct::new(Some("Updated Product".to_string()), None).unwrap(); let updated = service.update(created.id(), update).await.unwrap(); assert_eq!(updated.name(), "Updated Product"); assert_eq!(updated.description(), "Test Description"); } #[tokio::test] async fn test_product_service_delete() { let repo = MockRepository::::new(); let service = Service::new(repo); let create_product = CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap(); let created = service.create(create_product).await.unwrap(); service.delete(created.id()).await.unwrap(); assert!(service.get(created.id()).await.is_err()); } } mod error_tests { use super::*; #[tokio::test] async fn test_not_found_error() { let repo = MockRepository::::new(); let service = Service::new(repo); let result = service.get(Uuid::new_v4()).await; assert!(matches!(result, Err(ApplicationError::Domain(domain::DomainError::NotFound(_))))); } #[tokio::test] async fn test_update_nonexistent() { let repo = MockRepository::::new(); let service = Service::new(repo); let update = UpdateUser::new(Some("new_username".to_string()), None).unwrap(); let result = service.update(Uuid::new_v4(), update).await; assert!(matches!(result, Err(ApplicationError::Domain(domain::DomainError::NotFound(_))))); } #[tokio::test] async fn test_delete_nonexistent() { let repo = MockRepository::::new(); let service = Service::new(repo); let result = service.delete(Uuid::new_v4()).await; assert!(matches!(result, Err(ApplicationError::Domain(domain::DomainError::NotFound(_))))); } } }