Skill v1.0.1
currentAutomated scan96/1004 files
version: "1.0.1" name: rust-patterns description: Idiomatic Rust patterns, ownership, error handling, traits, concurrency, and best practices for building safe, performant applications. origin: ECC
Rust Development Patterns
Idiomatic Rust patterns and best practices for building safe, performant, and maintainable applications.
When to Use
- Writing new Rust code
- Reviewing Rust code
- Refactoring existing Rust code
- Designing crate structure and module layout
How It Works
This skill enforces idiomatic Rust conventions across six key areas: ownership and borrowing to prevent data races at compile time, Result/? error propagation with thiserror for libraries and anyhow for applications, enums and exhaustive pattern matching to make illegal states unrepresentable, traits and generics for zero-cost abstraction, safe concurrency via Arc<Mutex<T>>, channels, and async/await, and minimal pub surfaces organized by domain.
Core Principles
1. Ownership and Borrowing
Rust's ownership system prevents data races and memory bugs at compile time.
// Good: Pass references when you don't need ownershipfn process(data: &[u8]) -> usize {data.len()}// Good: Take ownership only when you need to store or consumefn store(data: Vec<u8>) -> Record {Record { payload: data }}// Bad: Cloning unnecessarily to avoid borrow checkerfn process_bad(data: &Vec<u8>) -> usize {let cloned = data.clone(); // Wasteful — just borrowcloned.len()}
Use Cow for Flexible Ownership
use std::borrow::Cow;fn normalize(input: &str) -> Cow<'_, str> {if input.contains(' ') {Cow::Owned(input.replace(' ', "_"))} else {Cow::Borrowed(input) // Zero-cost when no mutation needed}}
Error Handling
Use Result and ? — Never unwrap() in Production
// Good: Propagate errors with contextuse anyhow::{Context, Result};fn load_config(path: &str) -> Result<Config> {let content = std::fs::read_to_string(path).with_context(|| format!("failed to read config from {path}"))?;let config: Config = toml::from_str(&content).with_context(|| format!("failed to parse config from {path}"))?;Ok(config)}// Bad: Panics on errorfn load_config_bad(path: &str) -> Config {let content = std::fs::read_to_string(path).unwrap(); // Panics!toml::from_str(&content).unwrap()}
Library Errors with thiserror, Application Errors with anyhow
// Library code: structured, typed errorsuse thiserror::Error;#[derive(Debug, Error)]pub enum StorageError {#[error("record not found: {id}")]NotFound { id: String },#[error("connection failed")]Connection(#[from] std::io::Error),#[error("invalid data: {0}")]InvalidData(String),}// Application code: flexible error handlinguse anyhow::{bail, Result};fn run() -> Result<()> {let config = load_config("app.toml")?;if config.workers == 0 {bail!("worker count must be > 0");}Ok(())}
Option Combinators Over Nested Matching
// Good: Combinator chainfn find_user_email(users: &[User], id: u64) -> Option<String> {users.iter().find(|u| u.id == id).map(|u| u.email.clone())}// Bad: Deeply nested matchingfn find_user_email_bad(users: &[User], id: u64) -> Option<String> {match users.iter().find(|u| u.id == id) {Some(user) => match &user.email {email => Some(email.clone()),},None => None,}}
Enums and Pattern Matching
Model States as Enums
// Good: Impossible states are unrepresentableenum ConnectionState {Disconnected,Connecting { attempt: u32 },Connected { session_id: String },Failed { reason: String, retries: u32 },}fn handle(state: &ConnectionState) {match state {ConnectionState::Disconnected => connect(),ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),ConnectionState::Connecting { .. } => wait(),ConnectionState::Connected { session_id } => use_session(session_id),ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),ConnectionState::Failed { reason, .. } => log_failure(reason),}}
Exhaustive Matching — No Catch-All for Business Logic
// Good: Handle every variant explicitlymatch command {Command::Start => start_service(),Command::Stop => stop_service(),Command::Restart => restart_service(),// Adding a new variant forces handling here}// Bad: Wildcard hides new variantsmatch command {Command::Start => start_service(),_ => {} // Silently ignores Stop, Restart, and future variants}
Traits and Generics
Accept Generics, Return Concrete Types
// Good: Generic input, concrete outputfn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {let mut buf = Vec::new();reader.read_to_end(&mut buf)?;Ok(buf)}// Good: Trait bounds for multiple constraintsfn process<T: Display + Send + 'static>(item: T) -> String {format!("processed: {item}")}
Trait Objects for Dynamic Dispatch
// Use when you need heterogeneous collections or plugin systemstrait Handler: Send + Sync {fn handle(&self, request: &Request) -> Response;}struct Router {handlers: Vec<Box<dyn Handler>>,}// Use generics when you need performance (monomorphization)fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response {handler.handle(request)}
Newtype Pattern for Type Safety
// Good: Distinct types prevent mixing up argumentsstruct UserId(u64);struct OrderId(u64);fn get_order(user: UserId, order: OrderId) -> Result<Order> {// Can't accidentally swap user and order IDstodo!()}// Bad: Easy to swap argumentsfn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> {todo!()}
Structs and Data Modeling
Builder Pattern for Complex Construction
struct ServerConfig {host: String,port: u16,max_connections: usize,}impl ServerConfig {fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {ServerConfigBuilder { host: host.into(), port, max_connections: 100 }}}struct ServerConfigBuilder { host: String, port: u16, max_connections: usize }impl ServerConfigBuilder {fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self }fn build(self) -> ServerConfig {ServerConfig { host: self.host, port: self.port, max_connections: self.max_connections }}}// Usage: ServerConfig::builder("localhost", 8080).max_connections(200).build()
Iterators and Closures
Prefer Iterator Chains Over Manual Loops
// Good: Declarative, lazy, composablelet active_emails: Vec<String> = users.iter().filter(|u| u.is_active).map(|u| u.email.clone()).collect();// Bad: Imperative accumulationlet mut active_emails = Vec::new();for user in &users {if user.is_active {active_emails.push(user.email.clone());}}
Use collect() with Type Annotation
// Collect into different typeslet names: Vec<_> = items.iter().map(|i| &i.name).collect();let lookup: HashMap<_, _> = items.iter().map(|i| (i.id, i)).collect();let combined: String = parts.iter().copied().collect();// Collect Results — short-circuits on first errorlet parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();
Concurrency
Arc<Mutex<T>> for Shared Mutable State
use std::sync::{Arc, Mutex};let counter = Arc::new(Mutex::new(0));let handles: Vec<_> = (0..10).map(|_| {let counter = Arc::clone(&counter);std::thread::spawn(move || {let mut num = counter.lock().expect("mutex poisoned");*num += 1;})}).collect();for handle in handles {handle.join().expect("worker thread panicked");}
Channels for Message Passing
use std::sync::mpsc;let (tx, rx) = mpsc::sync_channel(16); // Bounded channel with backpressurefor i in 0..5 {let tx = tx.clone();std::thread::spawn(move || {tx.send(format!("message {i}")).expect("receiver disconnected");});}drop(tx); // Close sender so rx iterator terminatesfor msg in rx {println!("{msg}");}
Async with Tokio
use tokio::time::Duration;async fn fetch_with_timeout(url: &str) -> Result<String> {let response = tokio::time::timeout(Duration::from_secs(5),reqwest::get(url),).await.context("request timed out")?.context("request failed")?;response.text().await.context("failed to read body")}// Spawn concurrent tasksasync fn fetch_all(urls: Vec<String>) -> Vec<Result<String>> {let handles: Vec<_> = urls.into_iter().map(|url| tokio::spawn(async move {fetch_with_timeout(&url).await})).collect();let mut results = Vec::with_capacity(handles.len());for handle in handles {results.push(handle.await.unwrap_or_else(|e| panic!("spawned task panicked: {e}")));}results}
Unsafe Code
When Unsafe Is Acceptable
// Acceptable: FFI boundary with documented invariants (Rust 2024+)/// # Safety/// `ptr` must be a valid, aligned pointer to an initialized `Widget`.unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget {// SAFETY: caller guarantees ptr is valid and alignedunsafe { &*ptr }}// Acceptable: Performance-critical path with proof of correctness// SAFETY: index is always < len due to the loop boundunsafe { slice.get_unchecked(index) }
When Unsafe Is NOT Acceptable
// Bad: Using unsafe to bypass borrow checker// Bad: Using unsafe for convenience// Bad: Using unsafe without a Safety comment// Bad: Transmuting between unrelated types
Module System and Crate Structure
Organize by Domain, Not by Type
my_app/├── src/│ ├── main.rs│ ├── lib.rs│ ├── auth/ # Domain module│ │ ├── mod.rs│ │ ├── token.rs│ │ └── middleware.rs│ ├── orders/ # Domain module│ │ ├── mod.rs│ │ ├── model.rs│ │ └── service.rs│ └── db/ # Infrastructure│ ├── mod.rs│ └── pool.rs├── tests/ # Integration tests├── benches/ # Benchmarks└── Cargo.toml
Visibility — Expose Minimally
// Good: pub(crate) for internal sharingpub(crate) fn validate_input(input: &str) -> bool {!input.is_empty()}// Good: Re-export public API from lib.rspub mod auth;pub use auth::AuthMiddleware;// Bad: Making everything pubpub fn internal_helper() {} // Should be pub(crate) or private
Tooling Integration
Essential Commands
# Build and checkcargo buildcargo check # Fast type checking without codegencargo clippy # Lints and suggestionscargo fmt # Format code# Testingcargo testcargo test -- --nocapture # Show println outputcargo test --lib # Unit tests onlycargo test --test integration # Integration tests only# Dependenciescargo audit # Security auditcargo tree # Dependency treecargo update # Update dependencies# Performancecargo bench # Run benchmarks
Quick Reference: Rust Idioms
| Idiom | Description | |
|---|---|---|
| Borrow, don't clone | Pass &T instead of cloning unless ownership is needed | |
| Make illegal states unrepresentable | Use enums to model valid states only | |
? over unwrap() | Propagate errors, never panic in library/production code | |
| Parse, don't validate | Convert unstructured data to typed structs at the boundary | |
| Newtype for type safety | Wrap primitives in newtypes to prevent argument swaps | |
| Prefer iterators over loops | Declarative chains are clearer and often faster | |
#[must_use] on Results | Ensure callers handle return values | |
Cow for flexible ownership | Avoid allocations when borrowing suffices | |
| Exhaustive matching | No wildcard _ for business-critical enums | |
Minimal pub surface | Use pub(crate) for internal APIs |
Anti-Patterns to Avoid
// Bad: .unwrap() in production codelet value = map.get("key").unwrap();// Bad: .clone() to satisfy borrow checker without understanding whylet data = expensive_data.clone();process(&original, &data);// Bad: Using String when &str sufficesfn greet(name: String) { /* should be &str */ }// Bad: Box<dyn Error> in libraries (use thiserror instead)fn parse(input: &str) -> Result<Data, Box<dyn std::error::Error>> { todo!() }// Bad: Ignoring must_use warningslet _ = validate(input); // Silently discarding a Result// Bad: Blocking in async contextasync fn bad_async() {std::thread::sleep(Duration::from_secs(1)); // Blocks the executor!// Use: tokio::time::sleep(Duration::from_secs(1)).await;}
Remember: If it compiles, it's probably correct — but only if you avoid unwrap(), minimize unsafe, and let the type system work for you.