sharenet/backend/crates/application/src/lib.rs

387 lines
14 KiB
Rust

/*
* 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 <continuist02@gmail.com>
*/
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<T> = std::result::Result<T, ApplicationError>;
pub trait Repository<T: Entity>: Send + Sync {
fn create(&self, data: T::Create) -> impl Future<Output = Result<T>> + Send;
fn find_by_id(&self, id: Uuid) -> impl Future<Output = Result<T>> + Send;
fn find_all(&self) -> impl Future<Output = Result<Vec<T>>> + Send;
fn update(&self, id: Uuid, data: T::Update) -> impl Future<Output = Result<T>> + Send;
fn delete(&self, id: Uuid) -> impl Future<Output = Result<()>> + Send;
}
pub trait UseCase<T: Entity> {
fn create(&self, data: T::Create) -> impl Future<Output = AppResult<T>> + Send;
fn get(&self, id: Uuid) -> impl Future<Output = AppResult<T>> + Send;
fn list(&self) -> impl Future<Output = AppResult<Vec<T>>> + Send;
fn update(&self, id: Uuid, data: T::Update) -> impl Future<Output = AppResult<T>> + Send;
fn delete(&self, id: Uuid) -> impl Future<Output = AppResult<()>> + Send;
}
#[derive(Clone)]
pub struct Service<T: Entity, R: Repository<T> + Clone> {
repository: R,
_phantom: PhantomData<T>,
}
impl<T: Entity, R: Repository<T> + Clone> Service<T, R> {
pub fn new(repository: R) -> Self {
Self {
repository,
_phantom: PhantomData,
}
}
}
impl<T: Entity, R: Repository<T> + Clone> UseCase<T> for Service<T, R> {
fn create(&self, data: T::Create) -> impl Future<Output = AppResult<T>> + Send {
async move {
self.repository.create(data).await.map_err(ApplicationError::Domain)
}
}
fn get(&self, id: Uuid) -> impl Future<Output = AppResult<T>> + Send {
async move {
self.repository.find_by_id(id).await.map_err(ApplicationError::Domain)
}
}
fn list(&self) -> impl Future<Output = AppResult<Vec<T>>> + Send {
async move {
self.repository.find_all().await.map_err(ApplicationError::Domain)
}
}
fn update(&self, id: Uuid, data: T::Update) -> impl Future<Output = AppResult<T>> + Send {
async move {
self.repository.update(id, data).await.map_err(ApplicationError::Domain)
}
}
fn delete(&self, id: Uuid) -> impl Future<Output = AppResult<()>> + 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<T: Entity> {
data: Arc<RwLock<HashMap<Uuid, T>>>,
}
impl<T: Entity + Clone + Send + Sync> MockRepository<T> {
fn new() -> Self {
Self {
data: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl Repository<User> for MockRepository<User> {
fn create(&self, data: CreateUser) -> impl Future<Output = Result<User>> + 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<Output = Result<User>> + 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<Output = Result<Vec<User>>> + Send {
async move {
let guard = self.data.read().await;
Ok(guard.values().cloned().collect())
}
}
fn update(&self, id: Uuid, data: UpdateUser) -> impl Future<Output = Result<User>> + 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<Output = Result<()>> + 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<Product> for MockRepository<Product> {
fn create(&self, data: CreateProduct) -> impl Future<Output = Result<Product>> + 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<Output = Result<Product>> + 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<Output = Result<Vec<Product>>> + Send {
async move {
let guard = self.data.read().await;
Ok(guard.values().cloned().collect())
}
}
fn update(&self, id: Uuid, data: UpdateProduct) -> impl Future<Output = Result<Product>> + 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<Output = Result<()>> + 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::<User>::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::<User>::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::<User>::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::<User>::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::<User>::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::<Product>::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::<Product>::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::<Product>::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::<Product>::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::<Product>::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::<User>::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::<User>::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::<User>::new();
let service = Service::new(repo);
let result = service.delete(Uuid::new_v4()).await;
assert!(matches!(result, Err(ApplicationError::Domain(domain::DomainError::NotFound(_)))));
}
}
}