Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:25:50 +08:00
commit e8782848b3
10 changed files with 2504 additions and 0 deletions

View File

@@ -0,0 +1,279 @@
---
name: domain-layer-expert
description: Guides users in creating rich domain models with behavior, value objects, and domain logic. Activates when users define domain entities, business rules, or validation logic.
allowed-tools: Read, Grep
version: 1.0.0
---
# Domain Layer Expert Skill
You are an expert at designing rich domain models in Rust. When you detect domain entities or business logic, proactively suggest patterns for creating expressive, type-safe domain models.
## When to Activate
Activate when you notice:
- Entity or value object definitions
- Business validation logic
- Domain rules implementation
- Anemic domain models (just data, no behavior)
- Primitive obsession (using String/i64 for domain concepts)
## Domain Model Patterns
### Pattern 1: Value Objects
```rust
// ✅ Value object with validation
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Email(String);
impl Email {
pub fn new(email: String) -> Result<Self, ValidationError> {
if !email.contains('@') {
return Err(ValidationError::InvalidEmail("Missing @ symbol".into()));
}
if email.len() > 255 {
return Err(ValidationError::InvalidEmail("Too long".into()));
}
Ok(Self(email))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
// Implement TryFrom for ergonomics
impl TryFrom<String> for Email {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
```
### Pattern 2: Entity with Identity
```rust
#[derive(Debug, Clone)]
pub struct User {
id: UserId,
email: Email,
name: String,
status: UserStatus,
}
impl User {
pub fn new(email: Email, name: String) -> Self {
Self {
id: UserId::generate(),
email,
name,
status: UserStatus::Active,
}
}
// Domain behavior
pub fn deactivate(&mut self) -> Result<(), DomainError> {
if self.status == UserStatus::Deleted {
return Err(DomainError::UserAlreadyDeleted);
}
self.status = UserStatus::Inactive;
Ok(())
}
pub fn change_email(&mut self, new_email: Email) -> Result<(), DomainError> {
if self.status != UserStatus::Active {
return Err(DomainError::UserNotActive);
}
self.email = new_email;
Ok(())
}
// Getters
pub fn id(&self) -> &UserId { &self.id }
pub fn email(&self) -> &Email { &self.email }
}
```
### Pattern 3: Domain Events
```rust
#[derive(Debug, Clone)]
pub enum UserEvent {
UserCreated { id: UserId, email: Email },
UserDeactivated { id: UserId },
EmailChanged { id: UserId, old_email: Email, new_email: Email },
}
pub struct User {
id: UserId,
email: Email,
events: Vec<UserEvent>,
}
impl User {
pub fn new(email: Email) -> Self {
let id = UserId::generate();
let mut user = Self {
id: id.clone(),
email: email.clone(),
events: vec![],
};
user.record_event(UserEvent::UserCreated { id, email });
user
}
pub fn change_email(&mut self, new_email: Email) -> Result<(), DomainError> {
let old_email = self.email.clone();
self.email = new_email.clone();
self.record_event(UserEvent::EmailChanged {
id: self.id.clone(),
old_email,
new_email,
});
Ok(())
}
pub fn take_events(&mut self) -> Vec<UserEvent> {
std::mem::take(&mut self.events)
}
fn record_event(&mut self, event: UserEvent) {
self.events.push(event);
}
}
```
### Pattern 4: Business Rules
```rust
pub struct Order {
id: OrderId,
items: Vec<OrderItem>,
status: OrderStatus,
total: Money,
}
impl Order {
pub fn new(items: Vec<OrderItem>) -> Result<Self, DomainError> {
if items.is_empty() {
return Err(DomainError::EmptyOrder);
}
let total = items.iter().map(|item| item.total()).sum();
Ok(Self {
id: OrderId::generate(),
items,
status: OrderStatus::Pending,
total,
})
}
pub fn add_item(&mut self, item: OrderItem) -> Result<(), DomainError> {
if self.status != OrderStatus::Pending {
return Err(DomainError::OrderNotEditable);
}
self.items.push(item.clone());
self.total = self.total + item.total();
Ok(())
}
pub fn confirm(&mut self) -> Result<(), DomainError> {
if self.status != OrderStatus::Pending {
return Err(DomainError::OrderAlreadyConfirmed);
}
if self.total < Money::dollars(10) {
return Err(DomainError::MinimumOrderNotMet);
}
self.status = OrderStatus::Confirmed;
Ok(())
}
}
```
## Anti-Patterns to Avoid
### ❌ Primitive Obsession
```rust
// BAD: Using primitives everywhere
pub struct User {
pub id: String,
pub email: String,
pub age: i32,
}
fn create_user(email: String, age: i32) -> User {
// No validation, easy to pass wrong data
}
// GOOD: Domain types
pub struct User {
id: UserId,
email: Email,
age: Age,
}
impl User {
pub fn new(email: Email, age: Age) -> Result<Self, DomainError> {
// Validation already done in Email and Age types
Ok(Self {
id: UserId::generate(),
email,
age,
})
}
}
```
### ❌ Anemic Domain Model
```rust
// BAD: Domain is just data
pub struct User {
pub id: String,
pub email: String,
pub status: String,
}
// Business logic in service layer
impl UserService {
pub fn deactivate_user(&self, user: &mut User) {
user.status = "inactive".to_string();
}
}
// GOOD: Domain has behavior
pub struct User {
id: UserId,
email: Email,
status: UserStatus,
}
impl User {
pub fn deactivate(&mut self) -> Result<(), DomainError> {
if self.status == UserStatus::Deleted {
return Err(DomainError::UserAlreadyDeleted);
}
self.status = UserStatus::Inactive;
Ok(())
}
}
```
## Your Approach
When you see domain models:
1. Check for primitive obsession
2. Suggest value objects for domain concepts
3. Move validation into domain types
4. Add behavior methods to entities
5. Ensure immutability where appropriate
Proactively suggest rich domain patterns when you detect anemic models or primitive obsession.

View File

@@ -0,0 +1,537 @@
---
name: hexagonal-advisor
description: Reviews code architecture for hexagonal patterns, checks dependency directions, and suggests improvements for ports and adapters separation. Activates when users work with services, repositories, or architectural patterns.
allowed-tools: Read, Grep, Glob
version: 1.0.0
---
# Hexagonal Architecture Advisor Skill
You are an expert at hexagonal architecture (ports and adapters) in Rust. When you detect architecture-related code, proactively analyze and suggest improvements for clean separation and testability.
## When to Activate
Activate this skill when you notice:
- Service or repository trait definitions
- Domain logic mixed with infrastructure concerns
- Direct database or HTTP client usage in business logic
- Questions about architecture, testing, or dependency injection
- Code that's hard to test due to tight coupling
## Architecture Checklist
### 1. Dependency Direction
**What to Look For**:
- Domain depending on infrastructure
- Business logic coupled to frameworks
- Inverted dependencies
**Bad Pattern**:
```rust
// ❌ Domain depends on infrastructure (Postgres)
pub struct UserService {
db: PgPool, // Direct dependency on PostgreSQL
}
impl UserService {
pub async fn create_user(&self, email: &str) -> Result<User, Error> {
// Domain logic mixed with SQL
sqlx::query("INSERT INTO users...")
.execute(&self.db)
.await?;
Ok(user)
}
}
```
**Good Pattern**:
```rust
// ✅ Domain depends only on port trait
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn save(&self, user: &User) -> Result<(), DomainError>;
async fn find(&self, id: &UserId) -> Result<User, DomainError>;
}
pub struct UserService<R: UserRepository> {
repo: R, // Depends on abstraction
}
impl<R: UserRepository> UserService<R> {
pub fn new(repo: R) -> Self {
Self { repo }
}
pub async fn create_user(&self, email: &str) -> Result<User, DomainError> {
let user = User::new(email)?; // Domain validation
self.repo.save(&user).await?; // Infrastructure through port
Ok(user)
}
}
```
**Suggestion Template**:
```
Your domain logic directly depends on infrastructure. Create a port trait instead:
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn save(&self, user: &User) -> Result<(), DomainError>;
}
pub struct UserService<R: UserRepository> {
repo: R,
}
This allows you to:
- Test with mock implementations
- Swap implementations without changing domain
- Keep domain pure and framework-agnostic
```
### 2. Port Definitions
**What to Look For**:
- Missing trait abstractions for external dependencies
- Concrete types in domain services
- Inconsistent port patterns
**Good Port Patterns**:
```rust
// Driven Port (Secondary) - What domain needs
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn find(&self, id: &UserId) -> Result<User, DomainError>;
async fn save(&self, user: &User) -> Result<(), DomainError>;
async fn delete(&self, id: &UserId) -> Result<(), DomainError>;
}
// Driven Port for external services
#[async_trait]
pub trait EmailService: Send + Sync {
async fn send_welcome_email(&self, user: &User) -> Result<(), DomainError>;
}
// Driving Port (Primary) - What domain exposes
#[async_trait]
pub trait UserManagement: Send + Sync {
async fn register_user(&self, email: &str) -> Result<User, DomainError>;
async fn get_user(&self, id: &UserId) -> Result<User, DomainError>;
}
```
**Suggestion Template**:
```
Define clear port traits for your external dependencies:
// What your domain needs (driven port)
#[async_trait]
pub trait Repository: Send + Sync {
async fn operation(&self) -> Result<Data, Error>;
}
// What your domain exposes (driving port)
#[async_trait]
pub trait Service: Send + Sync {
async fn business_operation(&self) -> Result<Output, Error>;
}
```
### 3. Domain Purity
**What to Look For**:
- Framework types in domain models
- SQL, HTTP, or file I/O in domain logic
- Domain models with derive macros for serialization
**Bad Pattern**:
```rust
// ❌ Domain model coupled to frameworks
use sqlx::FromRow;
use serde::{Serialize, Deserialize};
#[derive(FromRow, Serialize, Deserialize)] // ❌ Infrastructure concerns
pub struct User {
pub id: i64, // ❌ Database type leaking
pub email: String,
pub created_at: chrono::DateTime<chrono::Utc>, // ❌ chrono in domain
}
```
**Good Pattern**:
```rust
// ✅ Pure domain model
pub struct User {
id: UserId, // Domain type
email: Email, // Domain value object
}
impl User {
pub fn new(email: String) -> Result<Self, ValidationError> {
let email = Email::try_from(email)?; // Domain validation
Ok(Self {
id: UserId::generate(),
email,
})
}
pub fn email(&self) -> &Email {
&self.email
}
}
// Adapter layer handles persistence
#[derive(sqlx::FromRow)]
struct UserRow {
id: i64,
email: String,
}
impl From<UserRow> for User {
fn from(row: UserRow) -> Self {
// Conversion in adapter layer
}
}
```
**Suggestion Template**:
```
Keep your domain models pure and framework-agnostic:
// Domain layer - no framework dependencies
pub struct User {
id: UserId,
email: Email,
}
// Adapter layer - handles framework concerns
#[derive(sqlx::FromRow)]
struct UserRow {
id: i64,
email: String,
}
impl From<UserRow> for User {
fn from(row: UserRow) -> Self {
// Convert database representation to domain
}
}
```
### 4. Adapter Implementation
**What to Look For**:
- Adapters not implementing ports
- Business logic in adapters
- Missing adapter layer
**Good Adapter Pattern**:
```rust
pub struct PostgresUserRepository {
pool: PgPool,
}
#[async_trait]
impl UserRepository for PostgresUserRepository {
async fn save(&self, user: &User) -> Result<(), DomainError> {
let row = UserRow::from(user); // Domain → Infrastructure
sqlx::query!(
"INSERT INTO users (id, email) VALUES ($1, $2)",
row.id,
row.email
)
.execute(&self.pool)
.await
.map_err(|e| DomainError::RepositoryError(e.to_string()))?;
Ok(())
}
async fn find(&self, id: &UserId) -> Result<User, DomainError> {
let row = sqlx::query_as!(
UserRow,
"SELECT id, email FROM users WHERE id = $1",
id.value()
)
.fetch_one(&self.pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => DomainError::UserNotFound(id.to_string()),
_ => DomainError::RepositoryError(e.to_string()),
})?;
Ok(User::from(row)) // Infrastructure → Domain
}
}
```
**Suggestion Template**:
```
Implement your ports in the adapter layer:
pub struct PostgresRepo {
pool: PgPool,
}
#[async_trait]
impl MyPort for PostgresRepo {
async fn operation(&self, data: &DomainType) -> Result<(), Error> {
// Convert domain → infrastructure
let row = DbRow::from(data);
// Perform infrastructure operation
sqlx::query!("...").execute(&self.pool).await?;
Ok(())
}
}
```
### 5. Testing Strategy
**What to Look For**:
- Lack of test doubles
- Tests requiring real database
- Untestable domain logic
**Good Testing Pattern**:
```rust
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
// Mock repository for testing
struct MockUserRepository {
users: HashMap<UserId, User>,
}
impl MockUserRepository {
fn new() -> Self {
Self { users: HashMap::new() }
}
fn with_user(mut self, user: User) -> Self {
self.users.insert(user.id().clone(), user);
self
}
}
#[async_trait]
impl UserRepository for MockUserRepository {
async fn save(&self, user: &User) -> Result<(), DomainError> {
// Mock implementation
Ok(())
}
async fn find(&self, id: &UserId) -> Result<User, DomainError> {
self.users
.get(id)
.cloned()
.ok_or(DomainError::UserNotFound(id.to_string()))
}
}
#[tokio::test]
async fn test_create_user() {
// Arrange
let mock_repo = MockUserRepository::new();
let service = UserService::new(mock_repo);
// Act
let result = service.create_user("test@example.com").await;
// Assert
assert!(result.is_ok());
}
}
```
**Suggestion Template**:
```
Create mock implementations for testing:
#[cfg(test)]
mod tests {
struct MockRepository {
// Test state
}
#[async_trait]
impl MyPort for MockRepository {
async fn operation(&self) -> Result<Data, Error> {
// Mock behavior
Ok(test_data())
}
}
#[tokio::test]
async fn test_domain_logic() {
let mock = MockRepository::new();
let service = MyService::new(mock);
let result = service.business_operation().await;
assert!(result.is_ok());
}
}
```
### 6. Composition Root
**What to Look For**:
- Dependency construction scattered throughout code
- Missing application composition
- Unclear wiring
**Good Pattern**:
```rust
// Application composition root
pub struct Application {
user_service: Arc<UserService<PostgresUserRepository>>,
order_service: Arc<OrderService<PostgresOrderRepository>>,
}
impl Application {
pub async fn new(config: &Config) -> Result<Self, Error> {
// Infrastructure setup
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&config.database_url)
.await?;
// Adapter construction
let user_repo = PostgresUserRepository::new(pool.clone());
let order_repo = PostgresOrderRepository::new(pool.clone());
// Service construction with dependencies
let user_service = Arc::new(UserService::new(user_repo));
let order_service = Arc::new(OrderService::new(order_repo));
Ok(Self {
user_service,
order_service,
})
}
pub fn user_service(&self) -> Arc<UserService<PostgresUserRepository>> {
self.user_service.clone()
}
}
// Main function
#[tokio::main]
async fn main() -> Result<(), Error> {
let config = load_config()?;
let app = Application::new(&config).await?;
// Wire up HTTP handlers with services
let router = Router::new()
.route("/users", post(create_user_handler))
.with_state(app);
// Start server
axum::Server::bind(&"0.0.0.0:3000".parse()?)
.serve(router.into_make_service())
.await?;
Ok(())
}
```
**Suggestion Template**:
```
Create a composition root that wires all dependencies:
pub struct Application {
services: /* your services */
}
impl Application {
pub async fn new(config: &Config) -> Result<Self, Error> {
// 1. Setup infrastructure
let pool = create_pool(&config).await?;
// 2. Create adapters
let repo = PostgresRepo::new(pool);
// 3. Create services with dependencies
let service = MyService::new(repo);
Ok(Self { service })
}
}
```
## Common Anti-Patterns
### Anti-Pattern 1: Anemic Domain
```rust
// ❌ BAD: Domain is just data, no behavior
pub struct User {
pub id: String,
pub email: String,
}
// Business logic in service instead of domain
impl UserService {
pub fn validate_email(&self, email: &str) -> bool {
email.contains('@') // Should be in domain
}
}
// ✅ GOOD: Domain has behavior
pub struct User {
id: UserId,
email: Email, // Email is a value object with validation
}
impl Email {
pub fn try_from(s: String) -> Result<Self, ValidationError> {
if !s.contains('@') {
return Err(ValidationError::InvalidEmail);
}
Ok(Self(s))
}
}
```
### Anti-Pattern 2: Leaky Abstractions
```rust
// ❌ BAD: Infrastructure details leak through port
#[async_trait]
pub trait UserRepository {
async fn find(&self, id: i64) -> Result<UserRow, sqlx::Error>;
// ^^^ ^^^^^^^ ^^^^^^^^^^^
// Database type DB struct DB error
}
// ✅ GOOD: Port uses domain types only
#[async_trait]
pub trait UserRepository {
async fn find(&self, id: &UserId) -> Result<User, DomainError>;
// ^^^^^^^ ^^^^ ^^^^^^^^^^^
// Domain type Domain Domain error
}
```
## Your Approach
1. **Detect**: Identify architecture-related code patterns
2. **Analyze**: Check dependency direction and separation
3. **Suggest**: Provide specific refactoring steps
4. **Explain**: Benefits of hexagonal architecture
## Communication Style
- Focus on dependency inversion principle
- Emphasize testability benefits
- Provide complete examples with traits and implementations
- Explain the "why" behind the pattern
- Suggest incremental refactoring steps
When you detect architectural issues, proactively suggest hexagonal patterns that will improve testability, maintainability, and flexibility.

View File

@@ -0,0 +1,210 @@
---
name: port-adapter-designer
description: Helps design port traits and adapter implementations for external dependencies. Activates when users need to abstract away databases, APIs, or other external systems.
allowed-tools: Read, Grep
version: 1.0.0
---
# Port and Adapter Designer Skill
You are an expert at designing ports (trait abstractions) and adapters (implementations) for hexagonal architecture in Rust. When you detect external dependencies or integration needs, proactively suggest port/adapter patterns.
## When to Activate
Activate when you notice:
- Direct usage of databases, HTTP clients, or file systems
- Need to swap implementations for testing
- External service integrations
- Questions about abstraction or dependency injection
## Port Design Patterns
### Pattern 1: Repository Port
```rust
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn find_by_id(&self, id: &UserId) -> Result<User, RepositoryError>;
async fn find_by_email(&self, email: &Email) -> Result<User, RepositoryError>;
async fn save(&self, user: &User) -> Result<(), RepositoryError>;
async fn delete(&self, id: &UserId) -> Result<(), RepositoryError>;
async fn list(&self, limit: usize, offset: usize) -> Result<Vec<User>, RepositoryError>;
}
```
### Pattern 2: External Service Port
```rust
#[async_trait]
pub trait PaymentGateway: Send + Sync {
async fn process_payment(&self, amount: Money, card: &CardDetails) -> Result<PaymentId, PaymentError>;
async fn refund(&self, payment_id: &PaymentId) -> Result<RefundId, PaymentError>;
async fn get_status(&self, payment_id: &PaymentId) -> Result<PaymentStatus, PaymentError>;
}
```
### Pattern 3: Notification Port
```rust
#[async_trait]
pub trait NotificationService: Send + Sync {
async fn send_email(&self, to: &Email, subject: &str, body: &str) -> Result<(), NotificationError>;
async fn send_sms(&self, phone: &PhoneNumber, message: &str) -> Result<(), NotificationError>;
}
```
## Adapter Implementation Patterns
### PostgreSQL Adapter
```rust
pub struct PostgresUserRepository {
pool: PgPool,
}
impl PostgresUserRepository {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}
#[async_trait]
impl UserRepository for PostgresUserRepository {
async fn find_by_id(&self, id: &UserId) -> Result<User, RepositoryError> {
let row = sqlx::query_as!(
UserRow,
"SELECT id, email, name FROM users WHERE id = $1",
id.as_str()
)
.fetch_one(&self.pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => RepositoryError::NotFound,
_ => RepositoryError::Database(e.to_string()),
})?;
Ok(User::try_from(row)?)
}
async fn save(&self, user: &User) -> Result<(), RepositoryError> {
sqlx::query!(
"INSERT INTO users (id, email, name) VALUES ($1, $2, $3)
ON CONFLICT (id) DO UPDATE SET email = $2, name = $3",
user.id().as_str(),
user.email().as_str(),
user.name()
)
.execute(&self.pool)
.await
.map_err(|e| RepositoryError::Database(e.to_string()))?;
Ok(())
}
}
```
### HTTP Client Adapter
```rust
pub struct StripePaymentGateway {
client: reqwest::Client,
api_key: String,
}
#[async_trait]
impl PaymentGateway for StripePaymentGateway {
async fn process_payment(&self, amount: Money, card: &CardDetails) -> Result<PaymentId, PaymentError> {
#[derive(Serialize)]
struct PaymentRequest {
amount: u64,
currency: String,
card: CardDetailsDto,
}
let response = self
.client
.post("https://api.stripe.com/v1/charges")
.bearer_auth(&self.api_key)
.json(&PaymentRequest {
amount: amount.cents(),
currency: amount.currency().to_string(),
card: CardDetailsDto::from(card),
})
.send()
.await
.map_err(|e| PaymentError::Network(e.to_string()))?;
if !response.status().is_success() {
return Err(PaymentError::GatewayRejected(response.status().to_string()));
}
let data: PaymentResponse = response
.json()
.await
.map_err(|e| PaymentError::ParseError(e.to_string()))?;
Ok(PaymentId::from(data.id))
}
}
```
### In-Memory Adapter (for testing)
```rust
pub struct InMemoryUserRepository {
users: Arc<Mutex<HashMap<UserId, User>>>,
}
impl InMemoryUserRepository {
pub fn new() -> Self {
Self {
users: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn with_users(users: Vec<User>) -> Self {
let map = users.into_iter().map(|u| (u.id().clone(), u)).collect();
Self {
users: Arc::new(Mutex::new(map)),
}
}
}
#[async_trait]
impl UserRepository for InMemoryUserRepository {
async fn find_by_id(&self, id: &UserId) -> Result<User, RepositoryError> {
self.users
.lock()
.await
.get(id)
.cloned()
.ok_or(RepositoryError::NotFound)
}
async fn save(&self, user: &User) -> Result<(), RepositoryError> {
self.users
.lock()
.await
.insert(user.id().clone(), user.clone());
Ok(())
}
}
```
## Port Design Guidelines
1. **Use domain types**: Parameters and return types should be domain objects
2. **Async by default**: Most I/O is async in Rust
3. **Return domain errors**: Convert infrastructure errors at the boundary
4. **Send + Sync**: Required for multi-threaded async runtimes
5. **Focused interfaces**: Each port should have a single responsibility
## Your Approach
When you see external dependencies:
1. Identify the interface needed
2. Design a port trait with domain types
3. Suggest adapter implementations
4. Show testing strategy with mocks
Proactively suggest port/adapter patterns when you detect tight coupling to external systems.