rules/rust/coding-style.md
This file extends common/coding-style.md with Rust-specific content.
cargo fmt before committingcargo clippy -- -D warnings (treat warnings as errors)Rust variables are immutable by default — embrace this:
let by default; only use let mut when mutation is requiredCow<'_, T> when a function may or may not need to allocateuse std::borrow::Cow;
// GOOD — immutable by default, new value returned
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input)
}
}
// BAD — unnecessary mutation
fn normalize_bad(input: &mut String) {
*input = input.replace(' ', "_");
}
Follow standard Rust conventions:
snake_case for functions, methods, variables, modules, cratesPascalCase (UpperCamelCase) for types, traits, enums, type parametersSCREAMING_SNAKE_CASE for constants and statics'a, 'de) — descriptive names for complex cases ('input)&T) by default; take ownership only when you need to store or consume&str over String, &[T] over Vec<T> in function parametersimpl Into<String> for constructors that need to own a String// GOOD — borrows when ownership isn't needed
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
// GOOD — takes ownership in constructor via Into
fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
// BAD — takes String when &str suffices
fn word_count_bad(text: String) -> usize {
text.split_whitespace().count()
}
Result<T, E> and ? for propagation — never unwrap() in production codethiserroranyhow for flexible error context.with_context(|| format!("failed to ..."))?unwrap() / expect() for tests and truly unreachable states// GOOD — library error with thiserror
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("failed to read config: {0}")]
Io(#[from] std::io::Error),
#[error("invalid config format: {0}")]
Parse(String),
}
// GOOD — application error with anyhow
use anyhow::Context;
fn load_config(path: &str) -> anyhow::Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {path}"))?;
toml::from_str(&content)
.with_context(|| format!("failed to parse {path}"))
}
Prefer iterator chains for transformations; use loops for complex control flow:
// GOOD — declarative and composable
let active_emails: Vec<&str> = users.iter()
.filter(|u| u.is_active)
.map(|u| u.email.as_str())
.collect();
// GOOD — loop for complex logic with early returns
for user in &users {
if let Some(verified) = verify_email(&user.email)? {
send_welcome(&verified)?;
}
}
Organize by domain, not by type:
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
pub(crate) for internal sharingpub what is part of the crate's public APIlib.rsSee skill: rust-patterns for comprehensive Rust idioms and patterns.