use std::{collections::HashMap, sync::Arc};
use allframe_core::arch::{domain, handler, repository, use_case};
use tokio::sync::Mutex;
#[tokio::test]
async fn test_full_clean_architecture_flow() {
#[domain]
#[derive(Clone)]
struct User {
id: String,
email: String,
name: String,
}
#[repository]
#[async_trait::async_trait]
trait UserRepository: Send + Sync {
async fn find(&self, id: &str) -> Option<User>;
async fn save(&self, user: User) -> Result<(), String>;
}
struct InMemoryUserRepository {
users: Mutex<HashMap<String, User>>,
}
impl InMemoryUserRepository {
fn new() -> Self {
Self {
users: Mutex::new(HashMap::new()),
}
}
}
#[async_trait::async_trait]
impl UserRepository for InMemoryUserRepository {
async fn find(&self, id: &str) -> Option<User> {
self.users.lock().await.get(id).cloned()
}
async fn save(&self, user: User) -> Result<(), String> {
self.users.lock().await.insert(user.id.clone(), user);
Ok(())
}
}
#[use_case]
struct GetUserUseCase {
repo: Arc<dyn UserRepository>,
}
impl GetUserUseCase {
pub async fn execute(&self, id: &str) -> Result<User, String> {
self.repo
.find(id)
.await
.ok_or_else(|| "User not found".to_string())
}
}
#[handler]
struct GetUserHandler {
use_case: Arc<GetUserUseCase>,
}
impl GetUserHandler {
pub async fn handle(&self, id: &str) -> Result<User, String> {
self.use_case.execute(id).await
}
}
let repo: Arc<dyn UserRepository> = Arc::new(InMemoryUserRepository::new());
repo.save(User {
id: "123".to_string(),
email: "user@example.com".to_string(),
name: "John Doe".to_string(),
})
.await
.unwrap();
let use_case = Arc::new(GetUserUseCase { repo });
let handler = GetUserHandler { use_case };
let user = handler.handle("123").await.unwrap();
assert_eq!(user.id, "123");
assert_eq!(user.email, "user@example.com");
assert_eq!(user.name, "John Doe");
}
#[tokio::test]
async fn test_multiple_use_cases_share_repository() {
#[domain]
#[derive(Clone)]
struct User {
id: String,
email: String,
}
#[repository]
#[async_trait::async_trait]
trait UserRepository: Send + Sync {
async fn find(&self, id: &str) -> Option<User>;
async fn save(&self, user: User) -> Result<(), String>;
async fn delete(&self, id: &str) -> Result<(), String>;
}
struct InMemoryUserRepository {
users: Mutex<HashMap<String, User>>,
}
#[async_trait::async_trait]
impl UserRepository for InMemoryUserRepository {
async fn find(&self, id: &str) -> Option<User> {
self.users.lock().await.get(id).cloned()
}
async fn save(&self, user: User) -> Result<(), String> {
self.users.lock().await.insert(user.id.clone(), user);
Ok(())
}
async fn delete(&self, id: &str) -> Result<(), String> {
self.users.lock().await.remove(id);
Ok(())
}
}
#[use_case]
struct GetUserUseCase {
repo: Arc<dyn UserRepository>,
}
#[use_case]
struct CreateUserUseCase {
repo: Arc<dyn UserRepository>,
}
#[use_case]
struct DeleteUserUseCase {
repo: Arc<dyn UserRepository>,
}
let repo: Arc<dyn UserRepository> = Arc::new(InMemoryUserRepository {
users: Mutex::new(HashMap::new()),
});
let get_use_case = GetUserUseCase { repo: repo.clone() };
let create_use_case = CreateUserUseCase { repo: repo.clone() };
let delete_use_case = DeleteUserUseCase { repo: repo.clone() };
create_use_case
.repo
.save(User {
id: "1".to_string(),
email: "test@example.com".to_string(),
})
.await
.unwrap();
let user = get_use_case.repo.find("1").await;
assert!(user.is_some());
assert_eq!(user.as_ref().unwrap().email, "test@example.com");
delete_use_case.repo.delete("1").await.unwrap();
let user = get_use_case.repo.find("1").await;
assert!(user.is_none());
}
#[tokio::test]
async fn test_handler_can_have_multiple_use_cases() {
#[domain]
#[derive(Clone)]
struct User {
id: String,
}
#[domain]
#[derive(Clone)]
struct Post {
id: String,
author_id: String,
}
#[repository]
#[async_trait::async_trait]
trait UserRepository: Send + Sync {
async fn find(&self, id: &str) -> Option<User>;
}
#[repository]
#[async_trait::async_trait]
trait PostRepository: Send + Sync {
async fn find_by_author(&self, author_id: &str) -> Vec<Post>;
}
struct InMemoryUserRepository {
users: Mutex<HashMap<String, User>>,
}
#[async_trait::async_trait]
impl UserRepository for InMemoryUserRepository {
async fn find(&self, id: &str) -> Option<User> {
self.users.lock().await.get(id).cloned()
}
}
struct InMemoryPostRepository {
posts: Mutex<Vec<Post>>,
}
#[async_trait::async_trait]
impl PostRepository for InMemoryPostRepository {
async fn find_by_author(&self, author_id: &str) -> Vec<Post> {
self.posts
.lock()
.await
.iter()
.filter(|p| p.author_id == author_id)
.cloned()
.collect()
}
}
#[use_case]
struct GetUserUseCase {
repo: Arc<dyn UserRepository>,
}
#[use_case]
struct GetUserPostsUseCase {
repo: Arc<dyn PostRepository>,
}
#[handler]
struct GetUserWithPostsHandler {
get_user: Arc<GetUserUseCase>,
get_posts: Arc<GetUserPostsUseCase>,
}
impl GetUserWithPostsHandler {
pub async fn handle(&self, user_id: &str) -> Result<(User, Vec<Post>), String> {
let user = self
.get_user
.repo
.find(user_id)
.await
.ok_or("User not found")?;
let posts = self.get_posts.repo.find_by_author(user_id).await;
Ok((user, posts))
}
}
let user_repo: Arc<dyn UserRepository> = Arc::new(InMemoryUserRepository {
users: Mutex::new(HashMap::from([(
"1".to_string(),
User {
id: "1".to_string(),
},
)])),
});
let post_repo: Arc<dyn PostRepository> = Arc::new(InMemoryPostRepository {
posts: Mutex::new(vec![Post {
id: "p1".to_string(),
author_id: "1".to_string(),
}]),
});
let get_user_uc = Arc::new(GetUserUseCase { repo: user_repo });
let get_posts_uc = Arc::new(GetUserPostsUseCase { repo: post_repo });
let handler = GetUserWithPostsHandler {
get_user: get_user_uc,
get_posts: get_posts_uc,
};
let (user, posts) = handler.handle("1").await.unwrap();
assert_eq!(user.id, "1");
assert_eq!(posts.len(), 1);
assert_eq!(posts[0].id, "p1");
}
#[test]
fn test_layer_metadata_available_at_runtime() {
use allframe_core::arch::LayerMetadata;
let domain_meta = LayerMetadata::new("domain", 1, "User");
let repo_meta = LayerMetadata::new("repository", 2, "UserRepository");
let use_case_meta = LayerMetadata::new("use_case", 3, "GetUserUseCase");
let handler_meta = LayerMetadata::new("handler", 4, "GetUserHandler");
assert_eq!(domain_meta.layer_name(), "domain");
assert_eq!(domain_meta.layer_number(), 1);
assert_eq!(domain_meta.type_name(), "User");
assert!(!domain_meta.can_depend_on("repository"));
assert!(repo_meta.can_depend_on("domain"));
assert!(use_case_meta.can_depend_on("repository"));
assert!(handler_meta.can_depend_on("use_case"));
}
#[test]
fn test_architecture_diagram_generation() {
let expected_diagram = r#"
graph TD
Handler[Layer 4: Handler]
UseCase[Layer 3: Use Case]
Repository[Layer 2: Repository]
Domain[Layer 1: Domain]
Handler --> UseCase
UseCase --> Repository
Repository --> Domain
"#;
assert!(expected_diagram.contains("Handler"));
assert!(expected_diagram.contains("UseCase"));
assert!(expected_diagram.contains("Repository"));
assert!(expected_diagram.contains("Domain"));
}