| name | rust-programming |
| globs | *.rs |
| description | Rust programming โ ownership, traits, generics, async/await, error handling, serde, tracing, macros, design patterns, and production best practices. Covers Rust 2024 edition, GATs, const generics, type state pattern, Pin/Unpin. ALWAYS use when writing or reviewing Rust code. ALWAYS use for Rust projects, libraries, and applications. ALWAYS read architecture.md for initial project planning or non-trivial refactoring.
|
Rust Programming Skill
Comprehensive Rust guidance: idiomatic patterns, ownership, generics/traits, async/await,
error handling, serde, tracing, macros, and production best practices. Current for Rust 2024 edition.
Navigation โ Supporting Files
| File | Contents |
|---|
| architecture.md | Section index, 10 architectural principles, 17 Architecture Decision Rules (LLM), workspace & crate organization, [workspace.dependencies] + [workspace.lints] inheritance, feature flag architecture, trait-based DI (Box vs generics), hexagonal/clean architecture in Rust, state management (LazyLock, connection pools, figment config), production patterns (graceful shutdown, health checks, metrics), growing architecture (3 stages: single crate โ lib+bin โ workspace), inter-component communication (channels decision guide, escalation path), refactoring signals table, anti-patterns catalog, facade crate pattern (ripgrep), enum-based polymorphism (vs dyn Trait), Tower Layer/Service composition (axum), two-stage CLI argument parsing (ripgrep) |
| architecture-examples.md | Complete worked examples: DI containers (shaku), domain modeling, resilience patterns, nanoservice architecture, async structured logging, multi-layer error translation |
| database.md | SQLx (compile-time queries, query_as!, connection pools, migrations), Diesel (schema DSL, associations), MongoDB, caching (in-memory with moka/dashmap, Redis with deadpool), query composition patterns |
| domain-patterns.md | DDD in Rust: entities, value objects (newtype), aggregates, domain events, event sourcing (state replay, snapshots, versioning), CQRS (command/query separation, read models, projections), bounded contexts as Cargo workspaces, anti-corruption layers |
| async-concurrency.md | Tokio runtime internals (work stealing, cooperative scheduling), Pin/Unpin explained, channels (mpsc, broadcast, watch, oneshot patterns), rayon for CPU-bound parallelism, Tower service pattern, actor patterns, structured concurrency, async closures (2024 ed.), graceful shutdown |
| error-handling.md | thiserror/anyhow/color-eyre/miette comparison, multi-layer error translation, error conversion chains across crate boundaries, production error reporting, recovery strategies, hand-rolled Error+ErrorKind pattern (ripgrep/tokio), error-value recovery (SendError<T>), uninhabited error types (NoError) |
| serde-serialization.md | Derive patterns with all common attributes, custom Serialize/Deserialize (Visitor pattern), enum representations (tagged, untagged, adjacently tagged), format-specific (JSON, TOML, bincode, CSV), zero-copy deserialization with Cow<'a, str>, DeserializeOwned vs Deserialize<'de> (lifetime distinction, three-tier string hierarchy) |
| web-apis.md | Axum (primary): extractors, middleware, routing, WebSocket, Tower integration. Rejection pattern (extractor error handling as responses). Actix Web, Rocket comparison. Authentication (JWT, Argon2, sessions), sqlx integration, reqwest HTTP client, CORS, static assets |
| testing.md | Unit/integration/E2E tests, mockall (mock traits, expectations), insta (snapshot testing), proptest (property-based), cargo-fuzz (fuzzing with arbitrary), async test patterns, database test fixtures, test organization, loom model checking (concurrency testing), compile-fail tests (trybuild, type safety verification) |
| cli-tools.md | clap derive and builder patterns, subcommands, value validation, shell completions. indicatif progress bars, crossterm terminal manipulation, prettytable output, file system operations, signal handling, non-fatal error accumulation (atomic flag pattern for parallel processing), lexopt alternative for complex CLIs (ripgrep pattern) |
| macros.md | Declarative macros (macro_rules!, repetition, TT muncher), procedural macros (TokenStream, syn, quote), derive macros (parsing struct/enum), attribute macros, when to use macros vs generics vs traits, real production macro examples |
| unsafe-ffi.md | Unsafe blocks, safety contracts, raw pointers. FFI with C (bindgen, cbindgen, repr(C)), CString/CStr, byte manipulation, endianness, network protocol parsing, AbortIfPanic guard (rayon pattern for critical unsafe sections) |
| deployment.md | Cargo profiles (LTO, codegen-units, strip, panic), Docker multi-stage builds (distroless images), CI/CD (GitHub Actions, GitLab CI), cross-compilation targets, structured logging with tracing (subscribers, layers, JSON output), metrics (prometheus, opentelemetry), custom cfg macros for feature flag management (tokio pattern) |
| data-structures.md | Rust-specific data structure patterns (Vec, HashMap, BTreeMap, VecDeque, BinaryHeap), algorithm implementations, benchmarking (criterion), profiling (flamegraph, perf, DHAT), memory profiling, optimization patterns (SmallVec, arrayvec, indexmap) |
| gui-wasm.md | egui (immediate mode), iced (Elm architecture), Leptos/Yew (web frontend), WASM (wasm-bindgen, wasm-pack, WASI), server-side Wasm, JS interop patterns |
| services.md | Microservices patterns (kernel pattern, feature-gated adapters), service discovery (Kubernetes, kube-rs), Redis caching and job queues, resilience (circuit breakers, retries, backoff, idempotency), CAP theorem, TCP server/client, TLS (rustls) |
| language-patterns.md | Everyday Rust idioms between SKILL.md basics and advanced type-system features. Pattern matching extended (match ergonomics, let-else, if-let chains, or-patterns), ownership patterns (borrow splitting, Cow<T>, zero-copy, entry API), ? operator chains with context, iterator composition (custom iterators, IntoIterator, lazy evaluation), closure capture semantics (Fn/FnMut/FnOnce), trait patterns (extension traits, blanket impls, orphan rule), From/Into/AsRef conversion hierarchy, RAII & Drop, module organization & visibility, conditional compilation, production patterns (config, graceful shutdown, retry, middleware), internal iteration (push-based callbacks, Producer/Consumer/Folder pattern from rayon) |
| documentation.md | Rustdoc conventions, doc comments (///, //!), doc test attributes (no_run, compile_fail, should_panic, hidden lines), intra-doc links ([Type], [Type::method]), standard sections (# Examples, # Errors, # Panics, # Safety), feature-gated docs (doc(cfg(...))), #[doc(hidden)]/#[doc(inline)], include_str! for external docs, docs.rs configuration, lints (missing_docs, broken_intra_doc_links), badge patterns, crate documentation architecture |
| type-system.md | Trait patterns (extension traits, blanket impls, orphan rule, object safety, supertraits), type conversions (From/Into/TryFrom/AsRef/Deref hierarchy, conversion decision guide), type state pattern deep dive (builder, protocol state machine), GATs (lending iterator, generic collection, type-parameterized), const generics (matrix, fixed buffer, defaults), Pin/Unpin (futures internals, pin projection, pin-project-lite), async traits (native vs async-trait, RPITIT, Rust 2024 capture rules), sealed traits, lifetime patterns (variance, PhantomData, HRTBs), marker type parameters for coherence (axum pattern), diagnostic attributes (do_not_recommend, on_unimplemented), compile-time trait bound assertions |
| quick-reference.md | Extended std/crate function reference (~300 methods): String, Vec, HashMap, Iterator, Option, Result, File/Path, formatting, common trait implementations (Display, FromStr, From/Into, AsRef, Deref, Index, IntoIterator, Drop), macros (std library, cfg, derive, attributes), chrono, regex, reqwest, tracing, clap, uuid, base64, anyhow/thiserror |
Rules for Writing Rust Code (LLM)
- ALWAYS use
Result for recoverable errors. Reserve unwrap() for tests, prototypes, and cases where the value is structurally guaranteed (e.g., Option::take() in a state machine). Use expect("reason") to document true invariants. In production, prefer ? or explicit error handling over blind unwrap() on fallible operations.
- ALWAYS propagate errors with
? operator for straightforward propagation. Add context with .map_err() or anyhow::Context when crossing module boundaries. Use explicit match when you need different logic per branch, not just to re-wrap and propagate.
- PREFER borrowing over cloning. Take
&str for read-only string params, &[T] for read-only slices. Use impl Into<String> or impl AsRef<str> for flexible public APIs (as clap and axum do). Take ownership when the function needs to store or move the data (builders, async tasks, struct fields).
- ALWAYS use iterators over manual index loops. Prefer
.iter(), .map(), .filter(), .collect() over for i in 0..len. Iterator chains are zero-cost abstractions and prevent off-by-one errors. Exception: manual indexing is appropriate for unsafe pointer arithmetic, circular buffer manipulation, or simultaneous multi-array traversal with complex index relationships.
- ALWAYS derive
Debug on public types. Derive Clone unless the type owns a unique resource (file handle, connection, runtime). Derive PartialEq when meaningful โ omit on types containing closures, trait objects, or I/O resources. Derive serde::Serialize/Deserialize for types crossing serialization boundaries. Use #[non_exhaustive] on public enums and error types โ this allows adding variants without breaking downstream callers (used by ripgrep, tokio, serde).
- PREFER
thiserror for library error types and anyhow for application error handling. Many major libraries (tokio, axum, hyper, ripgrep, serde) hand-roll impl Display + impl Error for full control over formatting, #[non_exhaustive], and patterns like Error { kind: ErrorKind } wrappers โ both approaches are valid and production-proven. Never use Box<dyn Error> in public APIs. Define specific error variants, not catch-all strings.
- NEVER use
String for error messages in Result. Use typed errors (Result<T, MyError>) so callers can match on variants. String errors lose information and prevent programmatic handling.
- ALWAYS mark long-running or I/O operations as
async when in an async context. Never block the async runtime with std::thread::sleep() or synchronous I/O โ use tokio::time::sleep() and async equivalents. Use tokio::task::spawn_blocking() for unavoidable blocking operations.
- Use appropriate synchronization for shared mutable state.
Arc<Mutex<T>> for simple cases, Arc<RwLock<T>> when reads vastly outnumber writes, dashmap for concurrent maps, parking_lot::Mutex for better performance. Use std::sync::Mutex (not tokio::sync::Mutex) unless you need to hold the lock across .await points. Never use Rc<RefCell<T>> across thread or .await boundaries (it is !Send).
- ALWAYS add a
// SAFETY: comment on unsafe blocks explaining why the invariants are upheld. Minimize unsafe surface area โ wrap unsafe in safe abstractions with clear contracts. Note: the clippy lint undocumented_unsafe_blocks is in the restriction category (opt-in), but top-tier projects (tokio, rust-analyzer) follow this practice consistently.
- ALWAYS use
clippy with a curated lint configuration. Use [workspace.lints.clippy] in Cargo.toml (stable since 1.74) to define project-specific lints โ this is the modern approach used by axum and other major projects. Avoid blanket clippy::pedantic (too noisy โ even serde suppresses dozens of its lints). Curate specific warn-level lints relevant to your project. Never blanket #[allow(clippy::all)].
- PREFER typed newtypes for domain values where primitive confusion is a real risk.
struct UserId(u64) prevents mixing up user IDs with order IDs. Use the newtype pattern for identifiers, validated values, and units. Raw primitives are fine for internal indices, sizes, and counters where confusion risk is low.
- ALWAYS use
tracing over log for new projects โ structured, span-aware, async-compatible. Used by tokio, axum, and sqlx. Use #[instrument] on key entry points where span context is valuable.
- ALWAYS specify
edition = "2024" in new Cargo.toml โ use the latest stable edition (stabilized in Rust 1.85.0) for improved RPIT lifetime captures, unsafe extern blocks, and gen keyword reservation. Note: async closures (also stabilized in 1.85.0) are edition-independent and work on all editions.
- PREFER
axum for new web APIs โ Tower-native, maintained by tokio team (tokio-rs/axum), dominant ecosystem adoption (~4x actix-web downloads). Actix Web 4.0+ works under #[tokio::main]; #[actix_web::main] is only needed for actor support.
- NEVER use
Rc<RefCell<T>> in async code โ it is !Send. Use Arc<Mutex<T>> or Arc<RwLock<T>> in any code that crosses .await points or thread boundaries. Rc<RefCell<T>> is only for single-threaded, synchronous code (or tokio::task::spawn_local).
- NEVER use
.clone() to silence the borrow checker without understanding why the borrow fails. Restructure ownership, use references, or scope borrows more tightly. Clone is appropriate for cheap-to-clone types (Arc, small structs) or when you genuinely need separate owned copies. Note: .clone() on Arc is idiomatic โ the Arc::clone(&x) form is a style preference, not a requirement.
- ALWAYS handle
JoinHandle results from tokio::spawn โ unwatched tasks that panic silently lose errors. Use JoinSet, store handles, or at minimum log spawn failures. Never fire-and-forget spawned tasks.
Thinking in Rust
Ownership as Resource Management โ Every value has exactly one owner. Design functions around who should own data. If a function doesn't need to keep the data, take a reference. If it does, take ownership. The ownership model replaces garbage collection AND prevents data races:
fn process(data: &[u8]) -> Summary { }
fn consume(data: Vec<u8>) -> Summary { }
fn share(data: Arc<Vec<u8>>) { }
Make Invalid States Unrepresentable โ Use the type system to prevent bugs at compile time. Newtypes prevent mixing up IDs. Enums prevent impossible states. Type state prevents calling methods in wrong order:
fn send_email(to: &str) { ... }
struct Email(String);
fn send_email(to: &Email) { ... }
Zero-Cost Abstractions โ Iterators, traits, generics, and closures compile to the same machine code as hand-written loops and switches. Don't avoid abstractions for "performance" โ use them for correctness and clarity.
Parse, Don't Validate โ Convert raw data into typed structures at system boundaries. Work with typed data internally. Validation returns bool (info discarded); parsing returns Result<T> (info preserved):
fn process(input: &str) -> Result<()> {
if !is_valid_email(input) { return Err(Error::Invalid); }
send_email(input);
Ok(())
}
fn process(input: &str) -> Result<()> {
let email = Email::parse(input)?;
send_email(&email);
Ok(())
}
Compiler as Collaborator โ When the borrow checker rejects your code, it's usually revealing a real problem (data race, use-after-free, aliased mutation). Restructure the code rather than fighting it. If you can't make it work, the design likely has a concurrency or ownership bug.
Imperative/OOP to Rust Translation
Collection Operations:
| C++/Java/Python | Idiomatic Rust |
|---|
for(i=0; i<len; i++) arr[i] | items.iter().enumerate() or .iter().map() |
result = []; for x in list: result.append(f(x)) | let result: Vec<_> = list.iter().map(f).collect(); |
list.filter(x => pred(x)) | list.iter().filter(|x| pred(x)).collect() |
acc = 0; for x in list: acc += x | list.iter().sum() or .fold(0, |acc, x| acc + x) |
list.find(x => x.id == target) | list.iter().find(|x| x.id == target) |
list.flatMap(x => x.children) | list.iter().flat_map(|x| &x.children).collect() |
[...new Set(list)] (deduplicate) | list.into_iter().collect::<HashSet<_>>() |
dict(zip(keys, values)) | keys.iter().zip(values).collect::<HashMap<_,_>>() |
Control Flow & Error Handling:
| C++/Java/Python | Idiomatic Rust |
|---|
try { risky() } catch(e) { handle(e) } | match risky() { Ok(v) => use(v), Err(e) => handle(e) } |
if (x == null) throw ... | let x = opt.ok_or(Error::Missing)?; |
x?.y?.z (optional chaining) | x.as_ref().and_then(|x| x.y.as_ref()).and_then(|y| y.z.as_ref()) |
switch(type) { case A: ... } | match value { Variant::A => ..., Variant::B => ... } |
throw new Exception("msg") | return Err(MyError::Variant) or bail!("msg") |
try { ... } finally { cleanup() } | RAII: Drop impl runs automatically, or scopeguard crate |
OOP Patterns to Rust:
| OOP | Rust Equivalent |
|---|
class Foo extends Bar | trait Bar {}; impl Bar for Foo {} โ composition over inheritance |
interface IService | trait Service { fn method(&self); } |
abstract class Base | Trait with default methods + required methods |
obj.field = value (mutate) | let new = Struct { field: value, ..old }; or &mut self methods |
new Foo() (constructor) | Foo::new() or Foo::builder().build() โ no special syntax |
Singleton.getInstance() | static INSTANCE: LazyLock<Foo> = LazyLock::new(|| ...); |
List<Animal> animals (polymorphism) | Vec<Box<dyn Animal>> or enum Animal { Dog(...), Cat(...) } |
private fields | All fields are private by default โ add pub explicitly |
instanceof check | matches!(value, Variant::A { .. }) or if let |
Ownership & Borrowing
The Three Rules
- Each value has exactly one owner
- When owner goes out of scope, value is dropped
- Ownership can be transferred (moved) or borrowed
let s1 = String::from("hello");
let s2 = s1;
let s3 = s2.clone();
println!("{} {}", s2, s3);
Move vs Copy
let x = 5;
let y = x;
let s1 = String::from("hello");
let s2 = s1;
Borrowing Rules
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
let r3 = &mut s;
r3.push_str(" world");
Lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
struct Excerpt<'a> {
text: &'a str,
}
let s: &'static str = "I live forever";
fn spawn_task<T: Send + 'static>(data: T) { }
Cow โ Clone on Write
use std::borrow::Cow;
fn maybe_uppercase(input: &str, shout: bool) -> Cow<str> {
if shout {
Cow::Owned(input.to_uppercase())
} else {
Cow::Borrowed(input)
}
}
fn normalize_path(path: &str) -> Cow<str> {
if path.contains("//") {
Cow::Owned(path.replace("//", "/"))
} else {
Cow::Borrowed(path)
}
}
Borrow Splitting
struct GameState {
player: Player,
enemies: Vec<Enemy>,
score: u32,
}
fn update(state: &mut GameState) {
let player = &mut state.player;
let enemies = &state.enemies;
player.update(enemies);
state.score += 1;
}
impl GameState {
fn update(&mut self) {
let p = &mut self.player;
let e = &self.enemies;
p.update(e);
}
}
Temporary Borrow Scoping
let mut data = vec![1, 2, 3, 4, 5];
let first = {
let slice = &data[..];
slice.first().copied()
};
data.push(6);
let result = {
let guard = mutex.lock().unwrap();
guard.clone()
};
do_async_work(result).await;
Deref Coercion
use std::ops::Deref;
struct Email(String);
impl Deref for Email {
type Target = str;
fn deref(&self) -> &str { &self.0 }
}
let email = Email("alice@example.com".into());
println!("{}", email.len());
println!("{}", email.contains('@'));
let boxed: Box<String> = Box::new("hello".into());
let s: &str = &boxed;
Deep dive: type-system.md โ Pin/Unpin internals (self-referential types,
why futures must be pinned), lifetime variance, subtyping, advanced Cow patterns, zero-copy parsing
with borrowed data, PhantomData uses beyond type state.
language-patterns.md โ Cow<T> in structs, zero-copy patterns (serde borrow,
string interning), entry API for complex map updates, From/Into/AsRef conversion hierarchy.
Pattern Matching
enum Command {
Quit,
Move { x: i32, y: i32 },
Write(String),
Color(u8, u8, u8),
}
fn handle(cmd: Command) {
match cmd {
Command::Quit => std::process::exit(0),
Command::Move { x, y } => println!("Move to ({x}, {y})"),
Command::Write(text) => println!("{text}"),
Command::Color(r, g, b) => println!("rgb({r}, {g}, {b})"),
}
}
match value {
n if n < 0 => println!("negative"),
id @ 1..=5 => println!("small: {id}"),
1 | 2 | 3 => println!("one two three"),
_ => println!("other"),
}
if let Some(v) = optional { use_value(v); }
while let Some(top) = stack.pop() { process(top); }
let ((x, y), Point { z, .. }) = (coords, point);
let-else โ Bind or Diverge
fn process(input: &str) -> Result<Output, Error> {
let Some(header) = input.lines().next() else {
return Err(Error::EmptyInput);
};
let Ok(config) = parse_header(header) else {
return Err(Error::InvalidHeader);
};
let Some(value) = config.get("key") else {
return Err(Error::MissingKey("key"));
};
Ok(Output::new(value))
}
if-let Chains (Rust 2024 Edition)
if let Some(user) = get_user(id)
&& let Some(email) = user.email.as_ref()
&& email.ends_with("@company.com")
{
send_internal_notification(email);
}
matches! Macro
let is_digit = matches!(ch, '0'..='9');
let is_keyword = matches!(word, "if" | "else" | "for" | "while" | "loop" | "match");
let is_small_positive = matches!(n, x if x > 0 && x < 100);
let has_errors = results.iter().any(|r| matches!(r, Err(_)));
let errors: Vec<_> = results.iter()
.filter(|r| matches!(r, Err(_)))
.collect();
let is_ok_and_even = matches!(result, Ok(n) if n % 2 == 0);
Deep dive: language-patterns.md โ or-patterns with bindings,
match ergonomics (auto-ref), match on references without moving, exhaustive matching strategies,
destructuring complex types (nested structs, slice patterns, tuple structs).
type-system.md โ type state pattern with exhaustive enum matching.
Type System
Generics
fn largest<T: PartialOrd>(list: &[T]) -> &T {
list.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap()
}
struct Pair<T, U> { first: T, second: U }
impl Pair<f64, f64> {
fn distance(&self) -> f64 {
(self.first.powi(2) + self.second.powi(2)).sqrt()
}
}
Traits
trait Summary {
fn summarize(&self) -> String;
fn preview(&self) -> String { format!("Read more: {}", self.summarize()) }
}
fn notify(item: &impl Summary) { }
fn notify<T: Summary>(item: &T) { }
fn notify<T>(item: &T) where T: Summary + Display { }
fn make_iter() -> impl Iterator<Item = i32> {
(0..10).filter(|n| n % 2 == 0)
}
Associated Types vs Generics
trait Container {
type Item;
fn get(&self, idx: usize) -> Option<&Self::Item>;
}
trait From<T> {
fn from(value: T) -> Self;
}
Trait Objects (Dynamic Dispatch)
fn render(components: &[Box<dyn Draw>]) {
for c in components { c.draw(); }
}
Extension Traits & Sealed Traits
trait StrExt {
fn shout(&self) -> String;
}
impl StrExt for str {
fn shout(&self) -> String { self.to_uppercase() + "!" }
}
mod private { pub trait Sealed {} }
pub trait MyTrait: private::Sealed {
fn method(&self);
}
struct Bytes(Vec<u8>);
impl TryFrom<u32> for Port {
type Error = PortError;
fn try_from(value: u32) -> Result<Self, Self::Error> {
if value > 65535 { Err(PortError::OutOfRange) } else { Ok(Port(value as u16)) }
}
}
RPITIT โ Return Position Impl Trait in Traits (stable 1.75+)
trait Repository {
async fn find(&self, id: u64) -> Option<Record>;
fn iter(&self) -> impl Iterator<Item = &Record>;
}
GATs โ Generic Associated Types (stable 1.65+)
trait LendingIterator {
type Item<'a> where Self: 'a;
fn next(&mut self) -> Option<Self::Item<'_>>;
}
struct WindowsMut<'a, T> {
data: &'a mut [T],
pos: usize,
}
impl<'a, T> LendingIterator for WindowsMut<'a, T> {
type Item<'b> = &'b mut [T] where Self: 'b;
fn next(&mut self) -> Option<Self::Item<'_>> {
if self.pos + 2 <= self.data.len() {
let window = &mut self.data[self.pos..self.pos + 2];
self.pos += 1;
Some(window)
} else {
None
}
}
}
Const Generics (stable 1.51+)
struct ArrayVec<T, const N: usize> {
data: [Option<T>; N],
len: usize,
}
impl<T, const N: usize> ArrayVec<T, N> {
fn new() -> Self {
Self { data: std::array::from_fn(|_| None), len: 0 }
}
fn push(&mut self, value: T) -> Result<(), T> {
if self.len >= N {
return Err(value);
}
self.data[self.len] = Some(value);
self.len += 1;
Ok(())
}
}
fn dot_product<const N: usize>(a: &[f64; N], b: &[f64; N]) -> f64 {
a.iter().zip(b).map(|(x, y)| x * y).sum()
}
Type State Pattern (Compile-Time State Machines)
use std::marker::PhantomData;
struct Draft;
struct Review;
struct Published;
struct Document<State> {
title: String,
content: String,
_state: PhantomData<State>,
}
impl Document<Draft> {
fn new(title: String) -> Self {
Self { title, content: String::new(), _state: PhantomData }
}
fn edit(&mut self, content: String) { self.content = content; }
fn submit(self) -> Document<Review> {
Document { title: self.title, content: self.content, _state: PhantomData }
}
}
impl Document<Review> {
fn approve(self) -> Document<Published> {
Document { title: self.title, content: self.content, _state: PhantomData }
}
fn reject(self) -> Document<Draft> {
Document { title: self.title, content: self.content, _state: PhantomData }
}
}
impl Document<Published> {
fn view(&self) -> &str { &self.content }
}
let doc = Document::<Draft>::new("RFC".into());
Higher-Rank Trait Bounds (HRTBs)
fn apply<F>(f: F) where F: for<'a> Fn(&'a str) -> &'a str {
let owned = String::from("hello");
println!("{}", f(&owned));
println!("{}", f("static"));
}
fn get_processor() -> Box<dyn for<'a> Fn(&'a str) -> usize> {
Box::new(|s| s.len())
}
Static vs Dynamic Dispatch Decision
| Criteria | impl Trait / Generics (Static) | dyn Trait (Dynamic) | Enum Dispatch |
|---|
| Set of types known at compile time? | Yes or no | Usually no | Yes (closed set) |
| Need heterogeneous collection? | No | Yes (Vec<Box<dyn T>>) | Yes (Vec<MyEnum>) |
| Performance critical? | Best (monomorphized, inlined) | Vtable overhead | Good (no vtable, match) |
| Binary size concern? | Larger (code per type) | Smaller | Compact (no monomorphization or vtable) |
| Adding new types? | Easy | Easy | Requires code changes |
| Object safety required? | No | Yes (no Self return, no generics) | No |
Rule of thumb: Start with generics/impl Trait. Use dyn Trait when you need heterogeneous collections or plugin-style extensibility. Use enum dispatch when you have a closed, known set of variants and want maximum performance.
String Type Decision
| Type | Use When |
|---|
&str | Function parameters, read-only string access, string literals |
String | Owned, growable strings โ struct fields, return values, building strings |
Cow<'a, str> | May or may not need to allocate โ parsers, config loaders, normalization |
&[u8] / Vec<u8> | Binary data, non-UTF-8 content, byte-level manipulation |
OsString / &OsStr | File paths, environment variables (may not be valid UTF-8) |
CString / &CStr | FFI with C (null-terminated, no interior nulls) |
Box<str> | Immutable owned string with exact allocation (no capacity overhead) |
Deep dive: type-system.md โ trait patterns (extension traits, blanket impls,
orphan rule, object safety rules, supertraits, trait composition, sealed traits deep dive),
type conversions (From/Into/TryFrom/AsRef/Deref hierarchy, conversion decision guide),
type state (builder with required fields, protocol state machine, when to use),
GATs (lending iterator, generic collection trait, type-parameterized associated types),
const generics (matrix, fixed buffer, defaults, nightly expressions),
Pin/Unpin (why futures are self-referential, pin projection, pin-project-lite),
async traits (native vs async-trait, RPITIT, Rust 2024 capture rules, trait_variant),
lifetime patterns (elision rules, variance, PhantomData uses, 'static misconceptions, HRTBs).
Error Handling
Rules for Error Handling (LLM)
- ALWAYS use
thiserror for library crates, anyhow for application crates. Libraries expose typed errors callers can match; applications just need context and propagation.
- NEVER use
String as an error type โ use typed error enums with variants. String errors lose all programmatic handling capability.
- ALWAYS add
.context() or .with_context() when propagating errors across module boundaries โ bare ? loses the "where" information.
- ALWAYS use
#[from] for automatic error conversion between layers. Define From impls or use #[from] in thiserror enums.
- PREFER matching specific error variants over catch-all handlers โ
Err(DbError::NotFound { .. }) not Err(e) => log(e).
- NEVER use
unwrap() in library code. Use expect("invariant reason") only for true invariants. In applications, prefer ? with context.
- ALWAYS use
ensure!() / bail!() from anyhow for early validation โ cleaner than manual if-return-Err.
- ALWAYS document error conditions in
/// doc comments for public functions โ which error variants can be returned and why.
Error Crate Decision
| Crate | Use When | Key Feature |
|---|
thiserror | Library crates, typed API errors | Derive Error with #[error], #[from], #[source] |
anyhow | Application crates, CLI tools | context(), bail!(), ensure!(), any error type |
color-eyre | Applications needing rich error reports | Colorized backtraces, span traces, custom sections |
miette | User-facing tools, diagnostics | Source code snippets, labels, help text in errors |
Rule: Use thiserror + anyhow together โ thiserror in your library crates, anyhow in your binary crate. They interoperate seamlessly.
#[derive(Debug, thiserror::Error)]
pub enum DbError {
#[error("record not found: {entity} id={id}")]
NotFound { entity: &'static str, id: String },
#[error("query failed: {query}")]
QueryFailed { query: String, #[source] cause: sqlx::Error },
#[error(transparent)]
Io(#[from] std::io::Error),
}
use anyhow::{Context, Result, bail, ensure};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("reading config from {path}"))?;
let config: Config = toml::from_str(&content)
.context("parsing config TOML")?;
ensure!(config.port > 0, "port must be positive, got {}", config.port);
Ok(config)
}
fn fetch_user(id: u64) -> Result<User, AppError> {
let config = load_config()?;
let user = db.find_user(id)?;
Ok(user)
}
fn nested_lookup(data: &HashMap<String, HashMap<String, i32>>) -> Option<i32> {
let inner = data.get("outer")?;
let value = inner.get("inner")?;
Some(*value)
}
fn load_user(id: u64) -> Result<User> {
db.find(id)
.with_context(|| format!("loading user {id} from database"))?
.ok_or_else(|| anyhow::anyhow!("user {id} not found"))
}
Error Conversion Across Layers
#[derive(Debug, thiserror::Error)]
pub enum RepoError {
#[error("not found: {entity} {id}")]
NotFound { entity: &'static str, id: String },
#[error(transparent)]
Database(#[from] sqlx::Error),
}
#[derive(Debug, thiserror::Error)]
pub enum ServiceError {
#[error(transparent)]
Repo(#[from] RepoError),
#[error("validation: {0}")]
Validation(String),
#[error("unauthorized")]
Unauthorized,
}
#[derive(Debug, thiserror::Error)]
pub enum HandlerError {
#[error(transparent)]
Service(#[from] ServiceError),
#[error("bad request: {0}")]
BadRequest(String),
}
async fn handle_request(id: u64) -> Result<Response, HandlerError> {
let user = user_service.find(id)?;
Ok(Response::ok(user))
}
Deep dive: error-handling.md โ color-eyre setup and customization,
miette diagnostic reports with source snippets, multi-layer error translation (handler โ service โ repo),
error conversion chains across crate boundaries, recovery strategies (retry, fallback, circuit breaker),
custom error context types, anyhow downcast patterns, collecting multiple errors (partition, validation).
Iterators & Closures
Iterators are Rust's primary abstraction for collection processing. They are zero-cost โ iterator chains compile to the same machine code as hand-written loops. Prefer iterators over manual indexing in all cases.
The Three Iterator Methods
let v = vec![1, 2, 3];
v.iter()
v.iter_mut()
v.into_iter()
for x in &v { }
for x in &mut v { }
for x in v { }
Iterator Adapters (Lazy)
Nothing happens until a consuming adapter is called โ adapters just build a pipeline:
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
let parsed: Vec<i32> = ["1", "two", "3"].iter()
.filter_map(|s| s.parse().ok())
.collect();
let words: Vec<&str> = ["hello world", "foo"].iter()
.flat_map(|s| s.split_whitespace())
.collect();
let all: Vec<i32> = (1..4).chain(10..13).collect();
let first_3: Vec<_> = numbers.iter().take(3).collect();
let skip_2: Vec<_> = numbers.iter().skip(2).collect();
let windows: Vec<_> = numbers.windows(2).collect();
let chunks: Vec<_> = numbers.chunks(2).collect();
let result: Vec<_> = numbers.iter()
.inspect(|x| println!("before filter: {x}"))
.filter(|x| **x > 2)
.inspect(|x| println!("after filter: {x}"))
.collect();
let mut iter = numbers.iter().peekable();
if iter.peek() == Some(&&1) { iter.next(); }
let running_sum: Vec<i32> = numbers.iter()
.scan(0, |state, &x| { *state += x; Some(*state) })
.collect();
Consuming Adapters
let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().sum();
let product: i32 = numbers.iter().product();
let max = numbers.iter().max();
let min = numbers.iter().min();
let count = numbers.iter().filter(|x| **x > 2).count();
let has_even = numbers.iter().any(|x| x % 2 == 0);
let all_pos = numbers.iter().all(|x| *x > 0);
let first_big = numbers.iter().find(|x| **x > 3);
let position = numbers.iter().position(|x| *x == 3);
let csv = numbers.iter().fold(String::new(), |mut acc, x| {
if !acc.is_empty() { acc.push(','); }
acc.push_str(&x.to_string());
acc
});
let result = numbers.iter().try_fold(0i32, |acc, &x| {
acc.checked_add(x).ok_or("overflow")
});
numbers.iter().for_each(|x| println!("{x}"));
let results: Result<Vec<i32>, _> = ["1", "2", "3"].iter()
.map(|s| s.parse::<i32>())
.collect();
use std::collections::HashMap;
let map: HashMap<&str, usize> = ["hello", "world"].iter()
.map(|s| (*s, s.len()))
.collect();
let (names, ages): (Vec<&str>, Vec<u32>) = [("Alice", 30), ("Bob", 25)]
.iter().copied().unzip();
let (evens, odds): (Vec<i32>, Vec<i32>) = numbers.into_iter()
.partition(|x| x % 2 == 0);
zip, enumerate, and Combining
let names = vec!["Alice", "Bob", "Charlie"];
let scores = vec![95, 87, 92];
for (i, name) in names.iter().enumerate() {
println!("{i}: {name}");
}
let ranked: Vec<_> = names.iter().zip(scores.iter()).collect();
let short = vec![1, 2];
let long = vec!["a", "b", "c", "d"];
let pairs: Vec<_> = short.iter().zip(long.iter()).collect();
Custom Iterator
struct Counter { count: u32, max: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max { self.count += 1; Some(self.count) } else { None }
}
}
let sum: u32 = Counter { count: 0, max: 5 }.sum();
Iterator Performance Patterns
let result: Vec<_> = Vec::with_capacity(items.len());
let mut result = Vec::new();
for item in items { result.push(transform(item)); }
let result: Vec<_> = items.iter().map(transform).collect();
let mut iter = numbers.iter();
let first_two: Vec<_> = iter.by_ref().take(2).collect();
let rest: Vec<_> = iter.collect();
Closures
let add = |a, b| a + b;
let mut count = 0;
let mut inc = || { count += 1; };
let consume = move || println!("{count}");
fn make_adder(x: i32) -> impl Fn(i32) -> i32 { move |y| x + y }
fn make_op(add: bool) -> Box<dyn Fn(i32, i32) -> i32> {
if add { Box::new(|a, b| a + b) } else { Box::new(|a, b| a - b) }
}
let name = String::from("Alice");
let greet = move || println!("Hello, {name}!");
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(f(x)) }
let result = apply_twice(|x| x + 3, 7);
Deep dive: language-patterns.md โ custom iterators, IntoIterator implementations,
iterator composition patterns, closure capture semantics (Fn/FnMut/FnOnce hierarchy).
data-structures.md โ iterator fusion, SIMD-friendly iteration with rayon.
async-concurrency.md โ async iterators, Stream trait, tokio StreamExt.
Collections & Data Structures
| Type | Use Case | Lookup | Insert |
|---|
Vec<T> | Ordered sequence, stack | O(n) | O(1) amortized push |
HashMap<K, V> | Key-value lookup | O(1) avg | O(1) avg |
BTreeMap<K, V> | Sorted key-value, range queries | O(log n) | O(log n) |
HashSet<T> | Membership testing | O(1) avg | O(1) avg |
VecDeque<T> | Double-ended queue | O(1) ends | O(1) ends |
BinaryHeap<T> | Priority queue | O(1) peek | O(log n) |
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("key", 42);
let val = map.get("key");
map.entry("key").or_insert(0);
*map.entry("counter").or_insert(0) += 1;
let mut word_count: HashMap<&str, usize> = HashMap::new();
for word in text.split_whitespace() {
word_count.entry(word)
.and_modify(|count| *count += 1)
.or_insert(1);
}
let mut connections: HashMap<String, Connection> = HashMap::new();
let conn = connections.entry(host.to_string())
.or_insert_with(|| {
Connection::new(&host)
.timeout(Duration::from_secs(30))
.build()
.expect("connection failed")
});
conn.send(message)?;
Deep dive: data-structures.md โ Vec internals (capacity, reallocation),
HashMap implementation (hashing, load factor), BTreeMap for range queries, VecDeque ring buffer,
BinaryHeap patterns, custom Hash implementations, IndexMap for insertion-order preservation,
SmallVec/ArrayVec for stack-allocated small collections, performance comparison benchmarks.
Strings & Slices
let s: &str = "hello";
let owned: String = s.to_string();
let borrowed: &str = &owned;
let mut s = String::with_capacity(100);
s.push_str("hello");
s.push('!');
let combined = format!("{} {}", s, "world");
use std::path::{Path, PathBuf};
let path = Path::new("/tmp/file.txt");
let ext = path.extension();
let mut buf = PathBuf::from("/tmp");
buf.push("file.txt");
Deep dive: quick-reference.md โ comprehensive String methods (split, trim, replace,
case, pad, find, chars, bytes), Path/PathBuf operations, OsString conversion patterns.
Serde Essentials
Rules for Serde (LLM)
- ALWAYS use
#[serde(rename_all = "camelCase")] on structs sent to/from JavaScript/JSON APIs โ Rust uses snake_case, JS uses camelCase.
- ALWAYS use
#[serde(skip_serializing_if = "Option::is_none")] on Option fields โ omit absent fields rather than serializing null.
- ALWAYS use
#[serde(default)] on fields that may be absent in input โ provides Default::default() rather than failing deserialization.
- PREFER internally-tagged enums (
#[serde(tag = "type")]) for most API enums โ produces {"type": "variant", ...} which is cleaner than externally-tagged {"variant": {...}}.
- ALWAYS derive both
Serialize and Deserialize unless there's a specific reason not to โ asymmetric serde is confusing and error-prone.
- PREFER
Cow<'a, str> over String in deserialization-heavy types โ enables zero-copy deserialization when possible.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiResponse {
user_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
avatar_url: Option<String>,
#[serde(default)]
is_active: bool,
#[serde(rename = "type")]
kind: String,
#[serde(flatten)]
metadata: HashMap<String, serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Event {
#[serde(rename = "click")]
Click { x: i32, y: i32 },
#[serde(rename = "key")]
KeyPress { key: String },
}
#[derive(Serialize, Deserialize)]
struct Config {
#[serde(deserialize_with = "deserialize_duration_secs")]
timeout: Duration,
}
Deep dive: serde-serialization.md โ custom Serialize/Deserialize (Visitor pattern),
all enum representations (externally tagged, internally tagged, adjacently tagged, untagged),
format-specific patterns (JSON, TOML, bincode, CSV), zero-copy deserialization with Cow<'a, str>,
#[serde(flatten)] for catch-all fields, #[serde(with)] for custom field serialization,
deserialize_with helpers, serde_json::Value for dynamic JSON.
Async/Await Core
Rules for Async Rust (LLM)
- NEVER block the async runtime โ no
std::thread::sleep(), no synchronous file I/O, no CPU-heavy computation in async functions. Use tokio::time::sleep(), tokio::fs, and tokio::task::spawn_blocking().
- ALWAYS handle JoinHandle results โ
tokio::spawn returns a JoinHandle. Store it, await it, or use JoinSet. Dropping it silently detaches the task.
- ALWAYS use bounded channels (
mpsc::channel(N)) โ unbounded channels can OOM under load. Size the buffer based on expected backpressure.
- NEVER hold a
MutexGuard across an .await point โ this blocks the runtime thread. Lock, extract data, drop the guard, then await.
- PREFER
tokio::select! with cancel safety โ understand which futures are cancel-safe. mpsc::Receiver::recv() is cancel-safe; read_to_string() is not.
- PREFER structured concurrency โ use
JoinSet or tokio::join! over raw tokio::spawn. Track and await all spawned tasks.
- ALWAYS use
spawn_blocking for CPU-bound work โ keeps the async executor free for I/O tasks. Threshold: >1ms of CPU work.
- PREFER channels over shared state โ channels provide natural backpressure and don't risk deadlocks. Use
Arc<Mutex<T>> only when you need synchronous shared state.
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
let body = reqwest::get(url).await?.text().await?;
Ok(body)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let result = fetch_data("https://api.example.com").await?;
Ok(())
}
use std::process::ExitCode;
fn main() -> ExitCode {
match run() {
Ok(true) => ExitCode::SUCCESS,
Ok(false) => ExitCode::from(1),
Err(err) => {
if is_broken_pipe(&err) { return ExitCode::SUCCESS; }
eprintln!("Error: {err:#}");
ExitCode::from(2)
}
}
}
let handle = tokio::spawn(async { expensive_work().await });
let result = handle.await?;
let (a, b) = tokio::join!(fetch("url1"), fetch("url2"));
tokio::select! {
result = fetch("url1") => handle(result),
_ = tokio::time::sleep(Duration::from_secs(5)) => timeout(),
}
let mut set = tokio::task::JoinSet::new();
for url in urls { set.spawn(async move { fetch(&url).await }); }
while let Some(result) = set.join_next().await { process(result?)?; }
Async Closures (Rust 2024 Edition)
let fetch = async |url: &str| -> Result<String, Error> {
reqwest::get(url).await?.text().await.map_err(Into::into)
};
async fn retry<F, Fut, T, E>(f: F, attempts: u32) -> Result<T, E>
where
F: Fn() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
for _ in 0..attempts {
if let Ok(v) = f().await { return Ok(v); }
}
f().await
}
NEVER Block the Runtime
async fn bad() {
std::thread::sleep(Duration::from_secs(1));
std::fs::read_to_string("file.txt").unwrap();
}
async fn good() -> Result<()> {
tokio::time::sleep(Duration::from_secs(1)).await;
tokio::fs::read_to_string("file.txt").await?;
Ok(())
}
let result = tokio::task::spawn_blocking(|| {
heavy_computation()
}).await?;
Deep dive: async-concurrency.md โ tokio runtime internals (work stealing,
cooperative scheduling), Pin/Unpin explained (why futures must be pinned, self-referential types),
channels (mpsc, broadcast, watch, oneshot โ patterns, sizing, backpressure), rayon for CPU-bound
parallelism (par_iter, join, scope), Tower service pattern (Service trait, Layer, middleware),
actor patterns (manual and with actix), structured concurrency, graceful shutdown patterns,
async closures (2024 edition), Stream trait and StreamExt.
Concurrency Primitives
When to Use Which Concurrency Primitive
| Need | Use | Why |
|---|
| Shared counter/flag | AtomicUsize / AtomicBool | Lock-free, cheapest option |
| Shared state, mostly reads | Arc<RwLock<T>> | Many concurrent readers, one writer |
| Shared state, frequent writes | Arc<Mutex<T>> or parking_lot::Mutex | Simpler than RwLock, less overhead |
| Concurrent HashMap | dashmap::DashMap | Sharded, no global lock |
| Producer-consumer | tokio::sync::mpsc | Bounded channel with backpressure |
| Broadcast to many consumers | tokio::sync::broadcast | Each subscriber gets every message |
| Latest-value config | tokio::sync::watch | Receivers see most recent value |
| One-shot response | tokio::sync::oneshot | Single value, single use |
| CPU-bound parallelism | rayon::par_iter() | Work stealing, automatic thread pool |
| Borrow stack data in threads | std::thread::scope | No Arc needed, threads must finish |
use std::sync::{Arc, Mutex, RwLock};
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
tokio::spawn(async move { *c.lock().unwrap() += 1; });
let cache = Arc::new(RwLock::new(HashMap::new()));
let data = cache.read().unwrap();
let mut data = cache.write().unwrap();
use std::sync::atomic::{AtomicUsize, Ordering};
static REQUESTS: AtomicUsize = AtomicUsize::new(0);
REQUESTS.fetch_add(1, Ordering::Relaxed);
std::thread::scope(|s| {
let data = vec![1, 2, 3];
s.spawn(|| println!("{:?}", &data[..2]));
s.spawn(|| println!("{:?}", &data[2..]));
});
use parking_lot::Mutex;
let data = Mutex::new(Vec::new());
data.lock().push(42);
Deep dive: async-concurrency.md โ channel patterns with complete examples,
actor pattern (message loop with mpsc), Mutex anti-patterns (holding across await, poisoning),
rayon parallel iterators and scopes, tokio task management (JoinSet, CancellationToken),
structured concurrency patterns, deadlock prevention.
Traits & API Design
Standard Trait Implementations
pub fn process(input: impl AsRef<str>) -> String { }
pub fn read_file(path: impl AsRef<Path>) -> io::Result<Vec<u8>> { }
pub fn set_name(&mut self, name: impl Into<String>) { self.name = name.into(); }
trait Pet: Animal + Display { fn cuddle(&self); }
impl From<ConfigFile> for AppConfig {
fn from(file: ConfigFile) -> Self { }
}
Blanket Implementations
impl<T: Display> ToString for T {
fn to_string(&self) -> String { format!("{self}") }
}
Marker Traits: Send, Sync, Sized
fn print_ref<T: ?Sized + Display>(value: &T) { println!("{value}"); }
print_ref("hello");
Library Authoring Patterns
Production libraries universally use these patterns (anyhow, serde_json, reqwest, axum, dashmap):
pub type Result<T, E = Error> = core::result::Result<T, E>;
#[cfg(test)]
fn _assert_traits() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn assert_clone<T: Clone>() {}
assert_send::<Client>();
assert_sync::<Client>();
assert_clone::<Client>();
}
#[doc(inline)]
pub use self::extract::Json;
#[doc(no_inline)]
pub use http::StatusCode;
#[doc(hidden)]
pub mod __private { }
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub fn from_json<T: DeserializeOwned>(s: &str) -> Result<T> { }
#[cfg(not(any(feature = "postgres", feature = "sqlite")))]
compile_error!("Enable either 'postgres' or 'sqlite' feature");
#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))]
#![cfg_attr(test, allow(clippy::print_stdout))]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#[cold]
fn handle_error(err: Error) -> Response { }
#[must_use = "this builder does nothing unless .build() is called"]
pub struct ClientBuilder { }
Deep dive: architecture.md โ trait-based dependency injection (generics vs trait objects),
SOLID principles in Rust (single responsibility, open-closed, Liskov, interface segregation, dependency inversion),
blanket implementations, extension traits for foreign types, API design guidelines.
type-system.md โ sealed traits, object safety rules, marker traits.
Modules & Cargo
Module System
mod config;
pub mod api;
pub(crate) mod internal;
pub use config::Config;
pub use api::{Client, Response};
pub mod prelude {
pub use crate::{Config, Client, Error};
}
#[cfg(feature = "json")]
mod json;
#[cfg(feature = "json")]
pub use json::Json;
pub use grep_cli as cli;
pub use grep_matcher as matcher;
#[cfg(feature = "pcre2")]
pub use grep_pcre2 as pcre2;
pub use grep_printer as printer;
#![forbid(unsafe_code)]
Cargo Workspaces
[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
thiserror = "2"
anyhow = "1"
[workspace.lints.rust]
missing_docs = "warn"
missing_debug_implementations = "warn"
unreachable_pub = "warn"
[workspace.lints.clippy]
dbg_macro = "warn"
print_stdout = "warn"
needless_pass_by_value = "warn"
type_complexity = "allow"
[dependencies]
serde.workspace = true
thiserror.workspace = true
[lints]
workspace = true
Feature Flags
[features]
default = ["json"]
json = ["dep:serde_json"]
postgres = ["dep:sqlx"]
full = ["json", "postgres"]
[dependencies]
serde_json = { version = "1.0", optional = true }
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio"], optional = true }
#[cfg(feature = "json")]
pub fn from_json(s: &str) -> Result<Config> { }
#[cfg(feature = "postgres")]
pub mod postgres;
Build Scripts & Profiles
[profile.release]
lto = true
debug = 1
[profile.dev]
opt-level = 0
debug = true
[profile.dev.package."*"]
opt-level = 2
[profile.release-lto]
inherits = "release"
lto = "fat"
codegen-units = 1
strip = true
panic = "abort"
debug-assertions = false
overflow-checks = false
[profile.deb]
inherits = "release-lto"
Project Layout Essentials
# Single crate (start here โ split only when you have a reason)
my_app/
โโโ Cargo.toml
โโโ src/
โ โโโ main.rs # Entry point
โ โโโ lib.rs # Library root (optional โ add when you need tests/benches)
โ โโโ config.rs # Configuration
โ โโโ models.rs # Domain types
โ โโโ handlers.rs # HTTP/CLI handlers
โ โโโ db.rs # Database access
โ โโโ errors.rs # Error types
โโโ tests/
โโโ integration.rs # Integration tests
# When to split into workspace: >10K lines, separate deploy targets,
# or team boundaries. See architecture.md for workspace layouts.
Trait-Based Dependency Injection (Key Pattern)
pub trait UserRepo: Send + Sync {
async fn find(&self, id: u64) -> Result<User, RepoError>;
async fn save(&self, user: &User) -> Result<(), RepoError>;
}
pub struct PgUserRepo { pool: PgPool }
impl UserRepo for PgUserRepo {
async fn find(&self, id: u64) -> Result<User, RepoError> { }
async fn save(&self, user: &User) -> Result<(), RepoError> { }
}
pub struct UserService<R: UserRepo> { repo: R }
pub struct UserService { repo: Arc<dyn UserRepo> }
Deep dive โ ALWAYS read for architecture planning and refactoring:
architecture.md โ 10 architectural principles, 15 Architecture Decision Rules (LLM),
workspace & crate organization, [workspace.dependencies] inheritance, feature flag architecture,
hexagonal/clean architecture in Rust, growing architecture (3 stages: single crate โ lib+bin โ workspace),
inter-component communication (channels, shared state), refactoring signals, anti-patterns catalog.
architecture-examples.md โ complete worked examples with full directory layouts.
Struct & Enum Patterns
Builder Pattern
Two styles: consuming (method chains, ergonomic) and borrow-based (reusable builder, ripgrep pattern):
#[derive(Default)]
struct ServerConfigBuilder {
host: Option<String>,
port: Option<u16>,
max_conn: Option<usize>,
}
impl ServerConfigBuilder {
fn host(mut self, host: impl Into<String>) -> Self { self.host = Some(host.into()); self }
fn port(mut self, port: u16) -> Self { self.port = Some(port); self }
fn max_conn(mut self, n: usize) -> Self { self.max_conn = Some(n); self }
fn build(self) -> Result<ServerConfig, &'static str> {
Ok(ServerConfig {
host: self.host.ok_or("host required")?,
port: self.port.unwrap_or(8080),
max_conn: self.max_conn.unwrap_or(100),
})
}
}
struct SearchWorkerBuilder {
search_zip: bool,
binary_detection: BinaryDetection,
}
impl SearchWorkerBuilder {
fn new() -> Self { Self { search_zip: false, binary_detection: BinaryDetection::default() } }
fn search_zip(&mut self, yes: bool) -> &mut Self { self.search_zip = yes; self }
fn preprocessor(&mut self, cmd: &Path) -> anyhow::Result<&mut Self> {
let _resolved = resolve_binary(cmd)?;
Ok(self)
}
fn build(&self) -> SearchWorker { }
}
Composable Query/Filter Pattern
Common in search engines (tantivy), query builders, and filter systems โ compose trait objects into boolean/logical trees:
use std::fmt::Debug;
trait Query: Debug { fn matches(&self, doc: &Document) -> bool; }
let mut clauses: Vec<(Occur, Box<dyn Query>)> = Vec::new();
for field in &search_fields {
let query = FuzzyTermQuery::new(field, &text, distance);
clauses.push((Occur::Should, Box::new(query)));
}
if let Some(filter) = category_filter {
clauses.push((Occur::Must, Box::new(ExactQuery::new("category", filter))));
}
let combined = BooleanQuery::new(clauses);
Newtype Pattern
struct UserId(u64);
struct OrderId(u64);
fn process(user: UserId, order: OrderId) { }
struct Email(String);
impl Email {
fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
let value = value.into();
if !value.contains('@') { return Err(ValidationError::InvalidEmail); }
Ok(Email(value))
}
fn as_str(&self) -> &str { &self.0 }
}
#[repr(transparent)]
struct Wrapper<T>(T);
Enum Dispatch
enum Shape {
Circle(f64),
Rectangle(f64, f64),
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
}
}
}
#[non_exhaustive]
pub enum Error {
NotFound,
PermissionDenied,
}
Deep dive: domain-patterns.md โ DDD entities and value objects (newtype pattern),
aggregates with invariant enforcement, enum-based state machines, command pattern, repository trait design.
architecture.md โ builder pattern with validation, configuration structs.
Smart Pointers
| Type | Use Case | Thread-Safe? |
|---|
Box<T> | Heap allocation, recursive types, trait objects | Send if T: Send |
Rc<T> | Multiple owners, single-threaded | No |
Arc<T> | Multiple owners, thread-safe | Yes |
RefCell<T> | Interior mutability, single-threaded | No |
Mutex<T> | Interior mutability, thread-safe | Yes |
RwLock<T> | Many readers OR one writer | Yes |
Cow<T> | Clone-on-write, avoid allocation | Depends on T |
Pin<P> | Prevent moves (required for self-referential futures) | Depends on P |
enum List { Cons(i32, Box<List>), Nil }
use std::pin::Pin;
let pinned: Pin<Box<dyn Future<Output = i32>>> = Box::pin(async { 42 });
Deep dive: type-system.md โ Pin/Unpin internals (why async futures
are self-referential, pin projection, Unpin auto-trait), when to use Box vs Rc vs Arc,
interior mutability patterns (Cell, RefCell, OnceCell), custom smart pointer implementations.
Common Mistakes (BAD/GOOD)
let user = db.find(id).unwrap(); | let user = db.find(id).context("finding user")?;
fn parse(s: &str) -> Result<C, String> | #[derive(thiserror::Error)] enum ParseError { ... }
fn f(data: &Vec<String>) -> String { | fn f(data: &[String]) -> String {
data.clone().join(", ") | data.join(", ")
} | }
for i in 0..items.len() { | let names: Vec<_> = items.iter()
if items[i].active { | .filter(|i| i.active)
results.push(items[i].name); | .map(|i| &i.name)
} | .collect();
} |
std::thread::sleep(dur); | tokio::time::sleep(dur).await;
std::fs::read_to_string(p).unwrap() | tokio::fs::read_to_string(p).await?
fn create(user_id: u64, order: u64) | fn create(user: UserId, order: OrderId)
unsafe { ptr::read(addr) } |
| unsafe { ptr::read(addr) }
items.iter().find(|u| u.id == id) | items.get(&id)
let data = Rc::new(vec![1, 2, 3]); | let data = Arc::new(vec![1, 2, 3]);
result = result + ∂ | result.push_str(&part);
static mut CONFIG: Option<C> = None; | static CONFIG: LazyLock<C> = LazyLock::new(|| { ... });
match opt { Some(v) => f(v), None => {} } | if let Some(v) = opt { f(v); }
trait DataProcessor<I,O,E> { ... } | fn process(input: &str) -> String { ... }
unsafe { } | fn safe_wrapper(p: *const u8, n: usize) -> &[u8] {
|
| unsafe { std::slice::from_raw_parts(p, n) }
| }
Extended BAD/GOOD Examples
Excessive boolean parameters โ config struct:
fn process(data: &str, verbose: bool, validate: bool, cache: bool) { ... }
process(data, true, false, true);
struct ProcessOptions { verbose: bool, validate: bool, cache: bool }
impl Default for ProcessOptions {
fn default() -> Self { Self { verbose: false, validate: true, cache: true } }
}
fn process(data: &str, opts: ProcessOptions) { ... }
process(data, ProcessOptions { verbose: true, ..Default::default() });
Stringly-typed APIs โ type-safe keys:
fn get_config(key: &str) -> Option<String> {
match key { "database.host" => Some("localhost".into()), _ => None }
}
let host = get_config("databse.host");
enum ConfigKey { DatabaseHost, DatabasePort }
fn get_config(key: ConfigKey) -> String {
match key {
ConfigKey::DatabaseHost => "localhost".into(),
ConfigKey::DatabasePort => "5432".into(),
}
}
Deadlock from inconsistent lock ordering:
fn transfer(from: &Mutex<Account>, to: &Mutex<Account>, amount: f64) {
let mut from_lock = from.lock().unwrap();
let mut to_lock = to.lock().unwrap();
from_lock.balance -= amount;
to_lock.balance += amount;
}
fn transfer(from: &Arc<Account>, to: &Arc<Account>, amount: f64) {
let (first, second) = if from.id < to.id { (from, to) } else { (to, from) };
let mut first_lock = first.balance.lock().unwrap();
let mut second_lock = second.balance.lock().unwrap();
if from.id < to.id {
*first_lock -= amount; *second_lock += amount;
} else {
*second_lock -= amount; *first_lock += amount;
}
}
Public error enum without #[non_exhaustive]:
#[derive(Debug)]
pub enum ErrorKind {
Io(std::io::Error),
Parse(String),
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ErrorKind {
Io(std::io::Error),
Parse(String),
}
Channel send losing the unsent value:
fn send(tx: &Sender<Message>, msg: Message) -> Result<(), Error> {
tx.send(msg).map_err(|_| Error::Closed)?;
Ok(())
}
pub struct SendError<T>(pub T);
impl<T> SendError<T> {
pub fn into_inner(self) -> T { self.0 }
}
Silently ignoring Result:
fn cleanup() {
let _ = std::fs::remove_file("temp.txt");
std::fs::read_to_string("config.txt");
}
fn cleanup() {
if let Err(e) = std::fs::remove_file("temp.txt") {
tracing::warn!("Could not remove temp file: {e}");
}
}
Use after move:
let s = String::from("hello");
let s2 = s;
let s = String::from("hello");
let s2 = &s;
println!("{s} {s2}");