AgentSkillsCN

rust-style

Rust 编码风格指南与架构模式。在编写、审查或修改 Rust 代码(.rs 文件)、创建新 Rust 项目,或解答有关本代码库中 Rust 规范的问题时使用。

SKILL.md
--- frontmatter
name: rust-style
description: Rust coding style guide and architecture patterns. Use when writing, reviewing, or modifying Rust code (.rs files), creating new Rust projects, or answering questions about Rust conventions in this codebase.

Rust Style Guide

Our approach to Rust: treat it like a high-level language to get most benefits with few downsides.

Philosophy

Write simple, readable code: types-first, immutable, functional-ish. Get the performance, memory safety, and portability benefits of Rust (80-90%) without fighting the borrow checker.

Core Principles

Immutable Data and Pure Functions

These rarely have issues with the borrow checker or lifetimes.

rust
// Good: pure function, easy to test and reason about
fn calculate_total(items: &[Item]) -> Decimal {
    items.iter().map(|i| i.price).sum()
}

// Good: immutable by default
let user = fetch_user(id)?;
let updated = User { name: new_name, ..user };
save_user(&updated)?;

Clone Liberally

Favor clarity over micro-optimization. Clone often - just be mindful of large objects.

rust
// Good: clear ownership, no lifetime complexity
fn process_order(order: Order) -> ProcessedOrder {
    // order is owned, can be transformed freely
}

// Avoid: fighting the borrow checker for marginal gains
fn process_order<'a>(order: &'a Order) -> ProcessedOrder<'a> {
    // lifetime annotations everywhere
}

Arc for Shared State

Use Arc<dyn Trait> for sharing services. This avoids lifetime annotations and works well with async.

rust
// Good: Arc provides 'static lifetime, works everywhere
pub struct AppState {
    pub user_service: Arc<dyn UserService>,
    pub order_service: Arc<dyn OrderService>,
}

// Services reference each other via traits
impl OrderServiceImpl {
    pub fn new(user_service: Arc<dyn UserService>) -> Self {
        Self { user_service }
    }
}

Domain Driven Design

Traits as Interfaces

Each service/repository is defined by a trait. Good for dependency injection and testing.

rust
#[async_trait]
pub trait UserRepository: Send + Sync {
    async fn find_by_id(&self, id: UserId) -> Result<Option<User>>;
    async fn save(&self, user: &User) -> Result<()>;
}

// Implementation
pub struct PgUserRepository {
    pool: PgPool,
}

#[async_trait]
impl UserRepository for PgUserRepository {
    // ...
}

Services at Context Root

Services are assembled at the application root and passed down via Arc<dyn Trait>.

rust
// In main.rs or app setup
let user_repo: Arc<dyn UserRepository> = Arc::new(PgUserRepository::new(pool.clone()));
let user_service: Arc<dyn UserService> = Arc::new(UserServiceImpl::new(user_repo));

let state = AppState { user_service };

Functional Domain Objects

Domain logic follows the Impure-Pure-Impure sandwich pattern:

code
Load (impure) -> Transform (pure) -> Save (impure)
rust
// Load
let order = order_repo.find_by_id(order_id).await?;

// Pure transform - easy to test
let updated = order.apply_discount(discount_code)?;

// Save
order_repo.save(&updated).await?;

Commands return new versions, not mutations:

rust
impl Order {
    // Returns new Order, doesn't mutate self
    pub fn apply_discount(self, code: DiscountCode) -> Result<Order> {
        let discount = code.calculate_discount(&self)?;
        Ok(Order {
            total: self.total - discount,
            applied_discounts: self.applied_discounts.with(code),
            ..self
        })
    }
}

Each request owns its data - no shared mutable state.

Testing Strategy

LayerApproach
Pure functionsUnit test thoroughly (data in, data out)
ServicesIntegration test with real DB or mocks at trait boundary
External servicesMock only (email, payments, third-party APIs)
rust
// Unit test pure logic
#[test]
fn apply_discount_reduces_total() {
    let order = Order::new(items, Decimal::new(100, 0));
    let result = order.apply_discount(ten_percent_code()).unwrap();
    assert_eq!(result.total, Decimal::new(90, 0));
}

// Integration test with mock at trait boundary
#[tokio::test]
async fn create_order_notifies_user() {
    let mock_notifier = Arc::new(MockNotifier::new());
    let service = OrderService::new(mock_notifier.clone());

    service.create_order(order_data).await.unwrap();

    assert!(mock_notifier.was_called_with(expected_notification));
}

What This Avoids

Pain PointHow We Avoid It
Lifetime annotationsArc everywhere for shared state
Generic explosiondyn Trait instead of monomorphization
Borrow checker fightsFunctional transforms, clone liberally
Async + lifetime painArc is 'static, works with async

What This Keeps

BenefitHow
Type safetyStrong types, discriminated unions
Exhaustive matchingEnums for state machines
No GCPredictable performance (2-3x faster than C#/F#, 10x faster than TS)
Explicit errorsResult types, no exceptions
Testable codePure functions, trait-based DI

Module Organization

Use the modern Rust 2018+ module style. Instead of mod.rs files, use a file alongside a directory with the same name.

Modern Style (Preferred)

rust
// src/users.rs declares the module and its public exports
pub mod users_routes;
pub mod users_service;
pub mod users_types;

pub use users_routes::*;
pub use users_service::UserService;
code
src/
├── users.rs              # Module declaration
└── users/                # Module contents
    ├── users_routes.rs
    ├── users_service.rs
    └── users_types.rs

Avoid: Old mod.rs Style

code
src/
└── users/
    ├── mod.rs            # Don't use this pattern
    ├── users_routes.rs
    └── ...

Why modern style is better:

  • File names are meaningful in editors/tabs (no more 5 tabs all named mod.rs)
  • Clearer which file corresponds to which module
  • Easier to navigate in file trees
  • The mod.rs style is a holdover from before Rust 2018

Vertical Slice Architecture

Organize code by feature (vertical slice) rather than by technical layer (horizontal). Each slice is a self-contained module with everything needed for that feature.

Structure

code
src/
├── main.rs                    # Entry point, assembles slices
├── core.rs                    # Core module declaration
├── core/                      # Cross-cutting concerns
│   ├── config.rs
│   ├── error.rs
│   ├── db.rs
│   └── views.rs
├── health.rs                  # Health slice declaration
├── health/                    # Health check slice
│   ├── health_routes.rs
│   └── health_service.rs
├── users.rs                   # Users slice declaration
├── users/                     # Example feature slice
│   ├── users_routes.rs
│   ├── users_service.rs
│   ├── users_data.rs
│   ├── users_types.rs
│   └── users_views.rs

Slice Contents

Each slice may contain (only include what's needed):

FilePurpose
*_routes.rsHTTP handlers, route definitions
*_service.rsBusiness logic, orchestration
*_data.rsDatabase queries, repository functions
*_types.rsSlice-specific types, DTOs
*_views.rsMaud HTML templates

Benefits

  • Cohesion: Related code lives together
  • Independence: Slices can be developed/tested in isolation
  • Discoverability: Find all user-related code in src/users/
  • Minimal coupling: Slices depend on core/, not each other

Guidelines

  • Start simple - a slice might just be *_routes.rs initially
  • Extract *_service.rs when business logic grows beyond route handlers
  • Extract *_data.rs when queries become complex or reusable
  • Shared infrastructure (config, errors, DB pool, layouts) goes in core/