rust-guide

SKILL.md

Rust Guide

Applies to: Rust 2021 edition+, Systems Programming, CLIs, WebAssembly, APIs

Core Principles

  1. Ownership Clarity: Every value has one owner; borrowing is explicit and intentional
  2. Result Over Panic: Return Result<T, E> for all fallible operations; panic! is a bug
  3. Zero-Cost Abstractions: Use iterators, generics, and traits without runtime overhead
  4. Minimal Unsafe: Default to #[forbid(unsafe_code)]; justify every unsafe block in writing
  5. Clippy Compliance: All code passes cargo clippy -- -D warnings with zero exceptions

Guardrails

Edition & Toolchain

  • Use Rust 2021 edition (edition = "2021" in Cargo.toml)
  • Set rust-version (MSRV) in Cargo.toml for all published crates
  • Use stable toolchain unless a nightly feature is explicitly justified
  • Pin dependency versions with ~ for compatible updates in libraries
  • Commit Cargo.lock for binaries; omit for libraries
  • Run cargo update weekly for security patches

Code Style

  • Run cargo fmt before every commit (no exceptions)
  • Run cargo clippy -- -D warnings before every commit
  • Follow Rust API Guidelines
  • snake_case for functions, variables, modules, crate names
  • PascalCase for types, traits, enums, type parameters
  • SCREAMING_SNAKE_CASE for constants and statics
  • Prefer exhaustive match over if let chains
  • No use super::* in non-test modules (explicit imports only)
  • Derive order: Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize

Error Handling

  • Return Result<T, E> for all operations that can fail
  • Use thiserror for library error types, anyhow for application code
  • Never use String as an error type (create enums or use thiserror)
  • Use ? operator for propagation; avoid manual match on Result when ? suffices
  • unwrap() is forbidden outside tests and examples
  • expect("reason") is allowed only with a comment explaining the invariant
  • Use #[must_use] on functions returning Result or important values

Lifetimes

  • Let the compiler infer lifetimes whenever possible (elision rules)
  • Add explicit annotations only when the compiler requires them
  • Prefer owned types in structs; use references only when zero-copy is measured to matter
  • Use Cow<'a, str> when a function may or may not allocate
  • Avoid 'static bounds unless truly needed (e.g., thread spawning, lazy statics)

Concurrency

  • Use tokio as the async runtime (unless project requires async-std)
  • All async operations must have timeouts via tokio::time::timeout
  • Use Arc<T> for shared ownership across threads; never Rc<T> in async code
  • Prefer tokio::sync::Mutex over std::sync::Mutex in async contexts
  • Use channels (mpsc, oneshot, broadcast) over shared mutable state
  • Spawn tasks with tokio::spawn; propagate errors via JoinHandle
  • Every select! branch must handle cancellation

Project Structure

Binary Crate

myproject/
├── Cargo.toml
├── Cargo.lock
├── src/
│   ├── main.rs              # Entry point, minimal (parse args, call run)
│   ├── lib.rs               # Public API and module declarations
│   ├── config.rs            # Configuration loading
│   ├── error.rs             # Custom error types
│   ├── domain/
│   │   ├── mod.rs
│   │   └── user.rs
│   └── infra/
│       ├── mod.rs
│       ├── db.rs
│       └── http.rs
├── tests/                   # Integration tests
│   └── api_test.rs
├── benches/                 # Benchmarks (criterion)
│   └── throughput.rs
└── examples/
    └── basic_usage.rs

Cargo Workspace

workspace/
├── Cargo.toml               # [workspace] members
├── crates/
│   ├── core/                # Domain logic (no I/O)
│   │   ├── Cargo.toml
│   │   └── src/lib.rs
│   ├── api/                 # HTTP layer
│   │   ├── Cargo.toml
│   │   └── src/lib.rs
│   └── cli/                 # Binary entry point
│       ├── Cargo.toml
│       └── src/main.rs
└── tests/                   # Workspace-level integration tests
  • main.rs should be thin: parse CLI args, build config, call lib.rs entry
  • Put all business logic in lib.rs modules (testable without binary)
  • Use workspaces for projects with 3+ crates
  • error.rs at crate root defines the crate-level error enum
  • No global mutable state; use dependency injection via function arguments or structs

Error Handling Patterns

Library Errors (thiserror)

use thiserror::Error;

#[derive(Error, Debug)]
pub enum StorageError {
    #[error("record {id} not found in {table}")]
    NotFound { table: &'static str, id: String },

    #[error("duplicate key: {0}")]
    Conflict(String),

    #[error(transparent)]
    Database(#[from] sqlx::Error),

    #[error(transparent)]
    Io(#[from] std::io::Error),
}

Application Errors (anyhow)

use anyhow::{bail, Context, Result};

fn load_config(path: &str) -> Result<Config> {
    let raw = std::fs::read_to_string(path)
        .with_context(|| format!("reading config from {path}"))?;

    let config: Config = toml::from_str(&raw)
        .context("parsing TOML config")?;

    if config.port == 0 {
        bail!("port must be non-zero");
    }

    Ok(config)
}

Conversion With the ? Operator

// Define From impls via thiserror's #[from], then just use ?
fn create_user(db: &Pool, input: &NewUser) -> Result<User, AppError> {
    validate_email(&input.email)?;   // ValidationError -> AppError
    let user = db.insert(input)?;    // sqlx::Error -> AppError
    Ok(user)
}

Testing

Unit Tests (in-module)

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_valid_email_succeeds() {
        let result = Email::parse("user@example.com");
        assert!(result.is_ok());
    }

    #[test]
    fn parse_invalid_email_returns_error() {
        let err = Email::parse("not-an-email").unwrap_err();
        assert!(matches!(err, ValidationError::InvalidEmail(_)));
    }

    #[test]
    fn amount_cannot_be_negative() -> Result<(), Box<dyn std::error::Error>> {
        let result = Amount::new(-5);
        assert!(result.is_err());
        Ok(())
    }
}

Integration Tests (tests/ directory)

// tests/api_test.rs
use myproject::app;

#[tokio::test]
async fn health_endpoint_returns_ok() {
    let app = app::build_test_app().await;
    let resp = app.get("/health").await;
    assert_eq!(resp.status(), 200);
}

Doc Tests

/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// use mycrate::add;
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Property-Based Testing (proptest)

use proptest::prelude::*;

proptest! {
    #[test]
    fn roundtrip_serialization(input in "[a-zA-Z0-9]{1,100}") {
        let encoded = encode(&input);
        let decoded = decode(&encoded)?;
        prop_assert_eq!(input, decoded);
    }
}

Testing Standards

  • Test names describe behavior: fn rejected_order_cannot_be_shipped()
  • Unit tests live in #[cfg(test)] mod tests in the same file
  • Integration tests go in tests/ directory
  • Use #[should_panic(expected = "...")] for panic-testing (rare)
  • Use assert!(matches!(...)) for enum variant assertions
  • Coverage target: >80% for libraries, >60% for applications
  • Use cargo tarpaulin or cargo llvm-cov for coverage
  • Async tests use #[tokio::test]

Tooling

Essential Commands

cargo fmt                       # Format (non-negotiable)
cargo clippy -- -D warnings     # Lint with deny
cargo test                      # All tests
cargo test --all-features       # Test all feature combinations
cargo check                     # Fast type-check (no codegen)
cargo doc --open                # Generate and view docs
cargo audit                     # Check for vulnerable deps
cargo deny check                # License + advisory check
cargo bench                     # Run benchmarks (criterion)

Cargo.toml Essentials

[package]
name = "myproject"
version = "0.1.0"
edition = "2021"
rust-version = "1.75"

[dependencies]
serde = { version = "1", features = ["derive"] }
thiserror = "2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tracing = "0.1"

[dev-dependencies]
proptest = "1"
tokio = { version = "1", features = ["test-util"] }

[profile.release]
lto = true
codegen-units = 1
strip = true

[lints.clippy]
pedantic = { level = "warn", priority = -1 }
unwrap_used = "deny"
expect_used = "warn"

Rustfmt Configuration

# rustfmt.toml
edition = "2021"
max_width = 100
use_field_init_shorthand = true

MSRV Policy

  • Set rust-version in Cargo.toml for every published crate
  • Test MSRV in CI: cargo +1.75.0 check
  • Bump MSRV only when a dependency or language feature requires it
  • Document MSRV bumps in changelog

Advanced Topics

For detailed patterns and examples, see:

External References

Weekly Installs
10
Repository
ar4mirez/samuel
GitHub Stars
3
First Seen
Feb 20, 2026
Installed on
opencode10
gemini-cli10
github-copilot10
codex10
amp10
kimi-cli10