| name | rust |
| description | Strict set of rules in terms of codebase development, design patterns, and best practices. Use when the user wants to develop a new feature or refactor existing code. |
Principles
Priority: Correctness > Safety > Readability > Performance
- Idiomatic Rust. Follow standard library conventions. If the stdlib does it one way, do it that way.
- Leverage the type system. Encode invariants in types, not runtime checks. Make illegal states unrepresentable.
- Domain-driven design. Name everything in the language of the problem, not the implementation.
- Reuse existing libraries and frameworks when possible.
Safety rules
Adapted from The Power of Ten (Holzmann) for Rust.
- Simple control flow. No nested if-else, no nested loops. Split into single-purpose functions. Max one level of branching per function body.
- Short functions. No function longer than ~60 lines. If it's too long, decompose it.
- Fixed loop bounds. All loops must have a provable upper bound. Prefer iterators (
for x in collection) over manual while i < len with index arithmetic.
- Minimal allocation. Prefer
&str over String, slices over Vec, borrows over clones. Allocate only when you must own.
- Assertions at boundaries. Use
debug_assert! for invariants within functions. Validate inputs at public API boundaries with Result/Option, not panics.
- Smallest possible scope. Variables, types, and functions should be visible only where needed. Prefer module-private by default; add
pub only when required.
- Handle all return values. Never discard
Result. Use ? propagation or explicit handling. Annotate intentional ignores with let _ =.
- Macros sparingly. Prefer generics, traits, and enums over procedural macros. Macros obscure control flow and complicate debugging.
- Domain types over raw primitives. Wrap repetitive low-level operations in a named type instead of scattering the same logic across functions.
let mut i = 0;
while i < bytes.len() {
if bytes[i] == target { return i; }
i += 1;
}
let mut scanner = Scanner::new(bytes);
while let Some(b) = scanner.peek() {
if b == target { return scanner.position(); }
scanner.advance();
}
- Zero warnings. Code must compile with
#[deny(warnings)] cleanly. Run cargo clippy and address all lints.
- Use
#[expect(lint, reason = "...")] instead of #[allow]. #[expect] warns when the suppression becomes unnecessary, preventing stale silencing. Always include a reason string. (M-LINT-OVERRIDE-EXPECT)
- Panics mean stop the program. Panics are not exceptions. Never use
panic!, unwrap(), or unreachable!() for recoverable errors — use Result. expect() is acceptable only for proven invariants with a descriptive message. (M-PANIC-IS-STOP)
unsafe only when required by 3rd-party libraries (e.g. PyO3 macros). No other reasons to write unsafe code.
Naming conventions
Sources: Rust API Guidelines C-CONV, C-GETTER; Microsoft M-CONCISE-NAMES
- Conversion prefixes follow ownership semantics:
as_ — cheap reference-to-reference (no allocation, no copy)
to_ — expensive conversion, may allocate (e.g. to_string())
into_ — consumes self, returns owned value
- No
get_ prefix on getters. Use fn name(&self) -> &str, not fn get_name().
- Implement
From<T>, never Into<T>. The blanket impl gives you Into for free.
- Concise type names. Avoid hollow suffixes:
Service, Manager, Factory, Handler, Processor. If the name needs a suffix, the type does too much.
- Named constants over magic literals. Every literal with domain meaning gets a
const with a doc comment. No bare numbers, bytes, or strings in logic.
if b == b'\\' { i += 2; }
// good
const ESCAPE_BYTE: u8 = b'\\';
if b == ESCAPE_BYTE { scanner.skip_escaped(); }
Error handling
Sources: Microsoft M-APP-ERROR, M-ERRORS-CANONICAL-STRUCTS
thiserror for library crates, anyhow/eyre for application crates. Libraries expose structured errors; apps just need context chains.
- Error messages: lowercase, no trailing punctuation. Matches
std convention for composable .context() chains.
- Use
? propagation everywhere. Avoid match on Result when ? + .map_err() suffices.
- Never
unwrap() in non-test code. Use expect("reason") only for proven invariants.
- Canonical error struct pattern:
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("invalid token at position {position}")]
InvalidToken { position: usize, token: char },
#[error("unexpected end of input")]
UnexpectedEof,
#[error(transparent)]
Io(#[from] std::io::Error),
}
Domain-driven design
Adapted from Domain-Driven Design (Evans/Fowler) for Rust.
Ubiquitous language
Name types, functions, and modules in the language of the problem domain, not the implementation.
fn find_end_offset(s: &str) -> Option<usize>
fn check_string(s: &str) -> bool
fn PaymentResult::validate(invoice: &Invoice) -> Option<PaymentResult>
fn ClassName::is_tailwind(token: &str) -> bool
Value objects as structs
Domain values without identity are structs. Functions take &Struct and return new structs — no mutation through output parameters.
fn process(input: &Config, out: &mut String)
fn process(input: &Config) -> ProcessResult
Enums for closed domain rules
When a domain has a fixed set of variants or checks, use an enum — not trait objects, not loose functions.
fn is_not_empty(s: &str) -> bool { ... }
fn starts_with_letter(s: &str) -> bool { ... }
enum ValidationRule { NonEmpty, StartsWithLetter, ContainsHyphen }
impl ValidationRule {
fn passes(self, input: &str) -> bool { match self { ... } }
}
const RULES: &[ValidationRule] = &[ValidationRule::NonEmpty, ...];
Traits for open abstractions
Use traits when behavior needs to be extended by future implementations. Start with the trait, then implement concrete types.
fn run_git_cmd() -> Output { ... }
fn run_uv_cmd() -> Output { ... }
trait ExternalCommand {
fn execute(&self) -> Result<Output>;
}
impl ExternalCommand for Git { ... }
impl ExternalCommand for Uv { ... }
See also Service & middleware and Trait object plugin in Ecosystem patterns.
Modules as bounded contexts
Each Rust module is a bounded context. Types and functions within a module share a domain model; the module boundary is the public API. Keep internal helpers private.
Sans-I/O: separate protocol logic from transport
Source: sans-io.readthedocs.io
Protocol logic (parsing, validation, state machines, data transformation) must be pure functions or types that take data in and return data out — no sockets, no channels, no async, no file handles. I/O operations (network, channels, disk) live in a thin outer layer that calls the protocol layer.
This makes protocol logic testable without standing up real infrastructure, reusable across different I/O backends (tokio, crossbeam, sync), and composable across boundaries (Rust ↔ Python).
impl SlotSend {
fn __call__(&self, py, event: &PyDict) -> PyResult<...> {
let type_val: String = event.get_item("type")?.extract()?;
match type_val.as_str() {
"http.response.start" => {
let status = event.get_item("status")?.extract()?;
*self.status.lock() = Some(status);
}
"http.response.body" => {
let body = event.get_item("body")?.extract()?;
self.outbound_tx.send(OutboundSlot { ... })?;
self.body_tx.send(body)?;
}
}
}
}
enum SendEvent {
Start { status: u16, headers: Vec<(Bytes, Bytes)> },
Body { data: Bytes, more_body: bool },
}
fn parse_send_event(event: &Bound<'_, PyDict>) -> PyResult<SendEvent> {
}
impl SlotSend {
fn __call__(&self, py, event: &PyDict) -> PyResult<...> {
let parsed = parse_send_event(event)?;
self.dispatch(parsed)
}
}
The same principle applies to request classification:
async fn handle(self, req: Request<Incoming>) -> Response<...> {
if req.uri().path() == "/_health/alive" {
return json_response(HEALTH_ALIVE);
}
if is_websocket_upgrade(&req) {
return self.dispatch.dispatch_ws(req).await;
}
}
enum RequestKind {
Probe(ProbeKind),
WebSocket,
Http,
}
fn classify(path: &str, headers: &HeaderMap) -> RequestKind {
}
async fn handle(self, req: Request<Incoming>) -> Response<...> {
match classify(req.uri().path(), req.headers()) {
RequestKind::Probe(kind) => probe_response(kind),
RequestKind::WebSocket => self.dispatch.dispatch_ws(req).await,
RequestKind::Http => self.dispatch_http(req).await,
}
}
Rule of thumb: if a function touches both data transformation AND a channel/socket/file, split it. The data transformation half is the protocol layer; the channel/socket half is the I/O layer. The protocol layer should be testable with #[test] using synthetic inputs — no #[tokio::test], no channels, no Python::attach.
API design
Sources: Microsoft M-INIT-BUILDER, M-IMPL-ASREF, M-IMPL-IO, M-AVOID-WRAPPERS; Rust API Guidelines C-COMMON-TRAITS
- Builder pattern for complex initialization. When a type has 4+ optional configuration fields, provide a builder instead of a constructor with many parameters.
- Accept
impl AsRef<str> / impl AsRef<Path> over concrete &str/String/&Path in function params when callers may have either type.
- Accept
impl Read / impl Write for I/O functions. Decouples logic from concrete I/O sources — enables testing with Cursor<Vec<u8>>.
- Avoid smart pointers in public APIs. Don't expose
Arc<Mutex<T>>, Box<T>, Rc<T> — let callers choose their wrapping strategy.
- Eagerly implement common traits:
Debug, Clone, PartialEq, Default on all public types. (C-COMMON-TRAITS)
- All public types must implement
Debug. No exceptions. (M-PUBLIC-DEBUG)
- Avoid unnecessary
Copy. Do not derive or implement Copy unless the type genuinely benefits from implicit copy semantics. Prefer Clone with explicit .clone() so copies are visible and intentional.
Code patterns
Return values, don't mutate
Functions return domain types instead of writing into &mut parameters. This makes data flow explicit and enables composition via .map(), .fold(), iterators.
fn transform(input: &str, out: &mut Vec<String>)
fn transform(input: &str) -> Vec<TransformResult>
Flat validation with early returns
Split complex validation into a scanning step and a checking step. Each is its own function. No nesting beyond one level.
if b == CLOSE {
if i + 1 < len && bytes[i + 1] == CLOSE {
if i == 0 { return None; }
return Some(i);
}
return None;
}
fn scan(input: &str) -> Option<Boundary> { ... }
fn validate(pos: usize, bytes: &[u8]) -> Option<Boundary> { ... }
Iterator chains over indexed loops
Prefer .iter(), .map(), .filter(), .collect() over for i in 0..len with manual indexing. Iterator chains are bounds-checked by construction.
Cow<'a, str> for conditional ownership