- Use
snafuexclusively — neverthiserroror manualimpl Error anyhowallowed at application boundaries (tool implementations, integrations, app bootstrap) but NOT in domain/core logic — usesnafuthere- Every error enum:
#[derive(Debug, Snafu)]+#[snafu(visibility(pub))] - Name:
{CrateName}Error, variants use#[snafu(display("..."))] - Propagate with
.context(XxxSnafu)?or.whatever_context("msg")? - Define
pub type Result<T> = std::result::Result<T, CrateError>per crate
- Trait objects: always create
pub type XRef = Arc<dyn X>alias - No hardcoded config defaults in Rust — all via YAML
Structs with 3+ fields MUST use #[derive(bon::Builder)] — do NOT write manual fn new() constructors.
Rules:
#[derive(bon::Builder)]on any struct with 3+ fields (config, domain objects, options, etc.)- Config structs: always pair with
Deserialize, never#[derive(Default)]— defaults come from YAML - Do NOT write
impl Foo { pub fn new(a, b, c, d, ...) -> Self }— use the generated builder instead - Cross-module construction: use
Foo::builder().field(val).build(), not struct literals - Within the defining module, struct literals are fine when all fields are straightforward
Option<T>fields automatically default toNonein bon — no need for#[builder(default)]- For non-Option defaults, use
#[builder(default = value)] - Simple 1-2 field structs can use direct construction (no builder needed)
// Good: derive builder + Deserialize for config
#[derive(Debug, Clone, bon::Builder, Serialize, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub max_connections: usize,
pub tls_enabled: bool,
}
// Good: construct via builder (especially from outside the module)
let config = ServerConfig::builder()
.host("0.0.0.0".into())
.port(8080)
.max_connections(100)
.tls_enabled(true)
.build();
// Bad: manual constructor — use the generated builder
impl ServerConfig {
pub fn new(host: String, port: u16, max_connections: usize, tls_enabled: bool) -> Self {
Self { host, port, max_connections, tls_enabled }
}
}#[async_trait]+Send + Syncbound on async trait definitions- Logging:
tracingmacros +#[instrument(skip_all)]
Prefer functional programming patterns over imperative code:
- Iterator chains over
forloops with manual accumulation — use.map(),.filter(),.flat_map(),.fold(),.collect() - Early returns with
?over nestedif let/match— keep the happy path flat - Combinators on Option/Result —
.map(),.and_then(),.unwrap_or_else(),.ok_or_else()overmatchwhen the logic is a simple transform matchfor complex branching — usematchwhen there are 3+ arms or when destructuring is needed; don't force combinators into unreadable chains- Closures for short inline logic; extract to named functions when the closure exceeds ~5 lines
- Immutable by default — only use
mutwhen mutation is genuinely needed letbindings for intermediate results — name intermediate values to improve readability rather than chaining everything into one expression- Avoid side effects in iterator chains — if you need side effects, use
foror.for_each()
// Good: functional chain
let active_names: Vec<_> = users
.iter()
.filter(|u| u.is_active)
.map(|u| &u.name)
.collect();
// Bad: imperative accumulation
let mut active_names = Vec::new();
for u in &users {
if u.is_active {
active_names.push(&u.name);
}
}
// Good: combinator on Option
let display = user.nickname.as_deref().unwrap_or(&user.name);
// Bad: match for simple default
let display = match &user.nickname {
Some(n) => n.as_str(),
None => &user.name,
};- Split logic into sub-files;
mod.rsonly for re-exports +//!module docs - Imports grouped:
std→ external crates → internal (crate::/super::) - No wildcard imports (
use foo::*) - All
pubitems must have///doc comments in English - Use
.expect("context")overunwrap()in non-test code