387 lines
14 KiB
Rust
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(_)))));
|
|
}
|
|
}
|
|
}
|