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

840 lines
27 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 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<T> = std::result::Result<T, DomainError>;
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<Utc>,
updated_at: DateTime<Utc>,
}
impl User {
// Constructor with validation
pub fn new(id: Uuid, username: String, email: String) -> Result<Self> {
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<Utc>, updated_at: DateTime<Utc>) -> 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<Utc> { self.created_at }
pub fn updated_at(&self) -> DateTime<Utc> { 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<Utc>,
updated_at: DateTime<Utc>,
}
impl Product {
// Constructor with validation
pub fn new(id: Uuid, name: String, description: String) -> Result<Self> {
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<Utc>, updated_at: DateTime<Utc>) -> 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<Utc> { self.created_at }
pub fn updated_at(&self) -> DateTime<Utc> { 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<Self> {
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<String>,
email: Option<String>,
}
impl UpdateUser {
pub fn new(username: Option<String>, email: Option<String>) -> Result<Self> {
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<String>) -> 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<String>) -> Result<()> {
self.email = email;
Ok(())
}
// Builder pattern methods
pub fn with_username(mut self, username: Option<String>) -> Result<Self> {
self.set_username(username)?;
Ok(self)
}
pub fn with_email(mut self, email: Option<String>) -> Result<Self> {
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<Self> {
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<String>,
description: Option<String>,
}
impl UpdateProduct {
pub fn new(name: Option<String>, description: Option<String>) -> Result<Self> {
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<String>) -> 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<String>) -> Result<()> {
self.description = description;
Ok(())
}
// Builder pattern methods
pub fn with_name(mut self, name: Option<String>) -> Result<Self> {
self.set_name(name)?;
Ok(self)
}
pub fn with_description(mut self, description: Option<String>) -> Result<Self> {
self.set_description(description)?;
Ok(self)
}
}
pub trait Repository<T, Create, Update>: Send + Sync
where
T: Send,
Create: Send,
Update: Send,
{
fn create(&self, data: 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: Update) -> impl Future<Output = Result<T>> + Send;
fn delete(&self, id: Uuid) -> impl Future<Output = Result<()>> + 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<T> {
data: Arc<RwLock<HashMap<Uuid, T>>>,
}
impl<T: Clone + Send + Sync> MockRepository<T> {
fn new() -> Self {
Self {
data: Arc::new(RwLock::new(HashMap::new())),
}
}
}
#[tokio::test]
async fn test_repository_create() {
let repo = MockRepository::<Product>::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::<Product>::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::<Product>::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::<Product>::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::<Product>::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());
}
}
}