| Work area | Specialized standard | Purpose |
|---|---|---|
| test code | /standardizing-rust-tests |
Rust test filenames, levels, evidence rules, doubles, harnesses, fixtures, and examples |
| ADRs and architecture docs | /standardizing-rust-architecture |
Rust architecture decision structure, testability constraints, and architecture review rules |
Keep examples in standardizing skills. Task skills such as /coding-rust, /testing-rust, and /auditing-rust-tests describe workflow and load order; the standardizing family owns reusable policy and concrete examples.
Rust code follows the repository's actual toolchain. Unless a repo-local overlay states otherwise:
- formatting uses
rustfmt - linting uses
clippy - compilation and tests run through
cargo - public APIs and boundaries use explicit types
These standards are enforced by compiler checks, linting, and code review together. Passing one tool is not enough if the code still violates the architectural intent below.
Use the Rust type system to encode meaning and constraints.
- prefer newtypes for domain identifiers and validated values
- use enums instead of stringly-typed state
- use
OptionandResultdeliberately; do not collapse them into booleans or sentinel values - keep public signatures explicit and stable
- make invalid states unrepresentable when the domain rules are stable
// preferred
pub struct UserId(u64);
pub enum JobStatus {
Pending,
Running,
Failed,
Complete,
}
// rejected
pub type UserId = u64;
pub type JobStatus = String;
Choose the right Rust construct for grouped constant values:
| Pattern | When to use |
|---|---|
const NAME: Type = value |
Simple scalar compile-time constant |
enum with as_str() or Display |
Closed set of string values with type safety — prefer over bare string constants |
OnceLock<HashMap<K, V>> (std) or Lazy<HashMap<K, V>> (once_cell) |
Map-like constants with complex initialization |
// ✅ preferred: enum for a closed set of string-valued states
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GateStatus {
Pass,
Fail,
Skipped,
}
impl GateStatus {
pub fn as_str(&self) -> &'static str {
match self {
GateStatus::Pass => "pass",
GateStatus::Fail => "fail",
GateStatus::Skipped => "skipped",
}
}
}
// ✅ preferred: OnceLock for a constant map
use std::collections::HashMap;
use std::sync::OnceLock;
static HEADERS: OnceLock<HashMap<&str, &str>> = OnceLock::new();
fn default_headers() -> &'static HashMap<&'static str, &'static str> {
HEADERS.get_or_init(|| {
let mut m = HashMap::new();
m.insert("Content-Type", "application/json");
m.insert("Accept", "application/json");
m
})
}
// ❌ rejected: scattered bare string constants without semantic structure
pub const STATUS_PASS: &str = "pass";
pub const STATUS_FAIL: &str = "fail";
pub const STATUS_SKIPPED: &str = "skipped";
No re-export of library constants. When production code and tests both need an HTTP status code, both import from the same canonical source (http::StatusCode, framework constants, etc.). Never create product-local aliases.
// ❌ rejected: product-local re-export
pub const HTTP_OK: u16 = 200;
// ✅ preferred: both production and test code import from the canonical source
use http::StatusCode;
let code = StatusCode::OK;
Ownership is a design tool, not a compiler obstacle.
- prefer single ownership by default
- borrow when the callee does not need ownership
- introduce
Arc,Rc,Mutex,RwLock, or interior mutability only when the use case requires them - avoid clone-driven designs that hide unclear data flow
- document long-lived shared state in architecture-level decisions
// preferred
pub fn render(config: &Config) -> Output { /* ... */ }
// suspicious unless justified
pub fn render(config: Config) -> Output {
let config = config.clone();
/* ... */
}
Error handling must preserve structure and intent.
- use typed errors at domain and library boundaries
- use
thiserrorfor structured public errors when it helps consumers - use
anyhowfor application orchestration when the boundary is not public - reserve
panic!,unwrap(), andexpect()for invariants, tests, or process-fatal startup requirements - include enough context for operators and callers to act
#[derive(thiserror::Error, Debug)]
pub enum LoadConfigError {
#[error("missing config file at {0}")]
Missing(PathBuf),
#[error("invalid config format")]
Invalid(#[source] serde_yaml::Error),
}
Use modules and visibility to enforce architecture.
- keep fields private unless external mutation is part of the contract
- expose constructors and behavior, not arbitrary mutation
- keep adapters thin and domain logic isolated from transport, storage, and CLI glue
- prefer traits at true architectural seams; avoid trait abstraction when a concrete type is enough
pub struct Account {
balance: Money,
}
impl Account {
pub fn credit(&mut self, amount: Money) {
self.balance += amount;
}
}
Async and concurrency choices need justification.
- use async for I/O-bound concurrency
- use threads or data parallelism for CPU-bound work
- do not hold locks across
.await - document
SendandSyncassumptions in shared state - prefer channels or ownership transfer over unnecessary shared mutable state
// preferred
let item = {
let mut guard = state.queue.lock().await;
guard.pop_front()
};
process(item).await;
Code should support evidence-rich tests without framework mocks.
- inject external process runners, clocks, and boundary adapters through traits or narrow function parameters
- use small hand-written doubles when a controlled implementation is needed
- keep pure logic separable from boundary glue
- do not make
mockallor similar generated mocks the default strategy
pub trait Clock {
fn now(&self) -> DateTime<Utc>;
}
pub struct Service<C: Clock> {
clock: C,
}
Unsafe code is a last-mile boundary, not a convenience feature.
- keep unsafe blocks narrow
- require
SAFETY:comments tied to the actual invariant - isolate FFI and raw pointer handling behind safe wrappers where possible
- prefer
MaybeUninit,NonNull, and typed wrappers over ad hoc pointer manipulation
// SAFETY: ptr is non-null, aligned, and valid for len initialized bytes.
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
Prefer Rust-native tools and idioms unless a repo-local overlay says otherwise:
clapfor CLI parsingserdefor serializationtracingfor structured observabilitytempfilefor tempdir-backed testsassert_cmdfor CLI L2 binary testsproptestorquickcheckfor property testingtrybuildfor compile-time contracts
| Anti-pattern | Why it is rejected |
|---|---|
| stringly-typed domain states | loses invariants and discoverability |
| clone-heavy data flow | hides unclear ownership design |
unwrap() in ordinary production paths |
turns expected failures into crashes |
| public mutable fields by default | breaks encapsulation and invariant control |
locks held across .await |
deadlock and contention risk |
| generated mocks as the default seam | weakens evidence and severs reality-based testing |
| unsafe used to bypass ownership design | replaces clear design with soundness risk |
Source: outcomeeng/plugins — distributed by TomeVault.