Rust Core
Shared model for the rust cluster. Both spokes — rust-patterns (write) and rust-testing
(verify) — depend on these conventions; keep them consistent here so the two never contradict
each other (e.g. an error type designed in code and asserted in a test).
1. The decision this cluster turns on: error strategy
Rust's error story splits on who consumes the error, and that single choice ripples into the
API, the call sites, and every test:
Library / reusable crate ──> thiserror (typed enum, #[from], stable variants — callers MATCH on it)
Application / binary ───────> anyhow (dynamic Result + .context() — callers REPORT it)
thiserror — derive a structured #[derive(Error)] enum at every library boundary. Callers
(and tests) pattern-match exact variants: assert!(matches!(err, StorageError::NotFound { .. })).
Use #[from] to absorb upstream errors without losing type. → rust-patterns
anyhow — in binaries and glue code, return anyhow::Result<T> and attach .context(...)
/.with_context(...) at each ?. Use bail!/ensure! for early exits. The trace, not the type,
is the product. → rust-patterns
- Never
Box<dyn Error> in a library, and never unwrap()/expect() on a recoverable
error in production. ? is the default; expect("…") is allowed only for genuinely-unreachable
invariants (poisoned mutex, joined thread) with a message that states the invariant.
Rule: decide lib-vs-app first; it fixes whether downstream code and tests match on variants
or just propagate context. Treat a change of error strategy (enum → dynamic, or vice versa) as a
public-API change.
2. Type-driven design conventions
The shared stance both spokes assume:
- Make illegal states unrepresentable — model variants as enums; match exhaustively, no
catch-all
_ for business logic (a new variant should force a compile error). → rust-patterns
- Parse, don't validate — convert unstructured input to typed structs at the boundary; wrap
primitives in newtypes (
UserId(u64)) so arguments can't be swapped. → rust-patterns
- Borrow, don't clone — pass
&T/&[T]/&str; reach for Cow when ownership is conditional.
- Accept generics, return concrete types; trait objects (
Box<dyn _>) only for heterogeneous
collections / plugins. → rust-patterns
- Minimal
pub surface — pub(crate) for internal sharing; re-export the public API from
lib.rs. Organize modules by domain, not by type.
unsafe is a last resort: every block carries a # Safety / // SAFETY: note proving its
invariants; never used to dodge the borrow checker.
3. Concurrency baseline
- Shared mutable state →
Arc<Mutex<T>> (treat lock() poisoning as an invariant via expect).
- Prefer message passing —
mpsc/sync_channel with bounded backpressure; drop(tx) to end an
rx iterator.
- Async on Tokio; never block the executor (
std::thread::sleep → tokio::time::sleep,
blocking I/O → spawn_blocking). Tests of async code use #[tokio::test]. → rust-patterns, rust-testing
4. Cargo / tooling matrix
| Concern |
Tool / command |
Spoke |
| Type-check (fast) |
cargo check |
rust-patterns |
| Lints |
cargo clippy -- -D warnings |
rust-patterns |
| Format |
cargo fmt --check |
rust-patterns |
| Security audit |
cargo audit |
rust-patterns |
| Unit / integration / doc tests |
cargo test (--lib / --test <name> / --doc) |
rust-testing |
| Parameterized tests |
rstest |
rust-testing |
| Property tests |
proptest |
rust-testing |
| Mocking |
mockall (#[automock]) |
rust-testing |
| Benchmarks |
criterion (harness = false) |
rust-testing |
| Coverage gate |
cargo llvm-cov --fail-under-lines 80 |
rust-testing |
CI runs them in order: fmt --check → clippy -D warnings → cargo test → coverage gate.
5. Version / conventions
- Target a current stable toolchain (
dtolnay/rust-toolchain@stable) with clippy + rustfmt
components; unsafe examples assume Rust 2024+ edition semantics.
- Pin dev-tooling versions in CI via
taiki-e/install-action (e.g. cargo-llvm-cov).
- Coverage targets: critical business logic 100%, public API 90%+, general code 80%+, generated/FFI
excluded.
6. Shared guardrails
? over unwrap() in all library/production code; expect("invariant") only for the truly
unreachable.
- Library →
thiserror, application → anyhow; never Box<dyn Error> in a library; state any
change to the error strategy.
- Make illegal states unrepresentable; match exhaustively; newtype your primitives.
- Keep
unsafe minimal and documented with a # Safety comment.
- Expose the narrowest
pub surface; organize by domain.
- TDD: write the failing test first; tests are independent, never
sleep(), and assert on
typed error variants (matches!) rather than panic strings wherever a Result is returned.
- Gate merges on
fmt --check, clippy -D warnings, cargo test, and the coverage threshold.
1---2name: rust-core3description: Shared reference for the Rust cluster: the library-vs-application error strategy (thiserror vs anyhow) that everything turns on, type-driven design conventions, the cargo tooling matrix, and the shared guardrails both spokes obey. USE WHEN choosing a Rust error type, shaping a public API, or wiring the cargo/CI toolchain — the decisions both patterns and testing depend on.4---56# Rust Core78Shared model for the `rust` cluster. Both spokes — `rust-patterns` (write) and `rust-testing`9(verify) — depend on these conventions; keep them consistent here so the two never contradict10each other (e.g. an error type designed in code and asserted in a test).1112## 1. The decision this cluster turns on: error strategy1314Rust's error story splits on **who consumes the error**, and that single choice ripples into the15API, the call sites, and every test:1617```18Library / reusable crate ──> thiserror (typed enum, #[from], stable variants — callers MATCH on it)19Application / binary ───────> anyhow (dynamic Result + .context() — callers REPORT it)20```2122- **`thiserror`** — derive a structured `#[derive(Error)]` enum at every library boundary. Callers23 (and tests) pattern-match exact variants: `assert!(matches!(err, StorageError::NotFound { .. }))`.24 Use `#[from]` to absorb upstream errors without losing type. → `rust-patterns`25- **`anyhow`** — in binaries and glue code, return `anyhow::Result<T>` and attach `.context(...)`26 /`.with_context(...)` at each `?`. Use `bail!`/`ensure!` for early exits. The trace, not the type,27 is the product. → `rust-patterns`28- **Never** `Box<dyn Error>` in a library, and **never** `unwrap()`/`expect()` on a recoverable29 error in production. `?` is the default; `expect("…")` is allowed only for genuinely-unreachable30 invariants (poisoned mutex, joined thread) with a message that states the invariant.3132**Rule:** decide lib-vs-app *first*; it fixes whether downstream code and tests match on variants33or just propagate context. Treat a change of error *strategy* (enum → dynamic, or vice versa) as a34public-API change.3536## 2. Type-driven design conventions3738The shared stance both spokes assume:3940- **Make illegal states unrepresentable** — model variants as enums; match exhaustively, no41 catch-all `_` for business logic (a new variant should force a compile error). → `rust-patterns`42- **Parse, don't validate** — convert unstructured input to typed structs at the boundary; wrap43 primitives in **newtypes** (`UserId(u64)`) so arguments can't be swapped. → `rust-patterns`44- **Borrow, don't clone** — pass `&T`/`&[T]`/`&str`; reach for `Cow` when ownership is conditional.45- **Accept generics, return concrete types**; trait objects (`Box<dyn _>`) only for heterogeneous46 collections / plugins. → `rust-patterns`47- **Minimal `pub` surface** — `pub(crate)` for internal sharing; re-export the public API from48 `lib.rs`. Organize modules **by domain, not by type**.49- **`unsafe`** is a last resort: every block carries a `# Safety` / `// SAFETY:` note proving its50 invariants; never used to dodge the borrow checker.5152## 3. Concurrency baseline5354- Shared mutable state → `Arc<Mutex<T>>` (treat `lock()` poisoning as an invariant via `expect`).55- Prefer message passing — `mpsc`/`sync_channel` with bounded backpressure; `drop(tx)` to end an56 `rx` iterator.57- Async on **Tokio**; never block the executor (`std::thread::sleep` → `tokio::time::sleep`,58 blocking I/O → `spawn_blocking`). Tests of async code use `#[tokio::test]`. → `rust-patterns`, `rust-testing`5960## 4. Cargo / tooling matrix6162| Concern | Tool / command | Spoke |63|---|---|---|64| Type-check (fast) | `cargo check` | `rust-patterns` |65| Lints | `cargo clippy -- -D warnings` | `rust-patterns` |66| Format | `cargo fmt --check` | `rust-patterns` |67| Security audit | `cargo audit` | `rust-patterns` |68| Unit / integration / doc tests | `cargo test` (`--lib` / `--test <name>` / `--doc`) | `rust-testing` |69| Parameterized tests | `rstest` | `rust-testing` |70| Property tests | `proptest` | `rust-testing` |71| Mocking | `mockall` (`#[automock]`) | `rust-testing` |72| Benchmarks | `criterion` (`harness = false`) | `rust-testing` |73| Coverage gate | `cargo llvm-cov --fail-under-lines 80` | `rust-testing` |7475CI runs them in order: `fmt --check` → `clippy -D warnings` → `cargo test` → coverage gate.7677## 5. Version / conventions7879- Target a **current stable** toolchain (`dtolnay/rust-toolchain@stable`) with `clippy` + `rustfmt`80 components; `unsafe` examples assume **Rust 2024+** edition semantics.81- Pin dev-tooling versions in CI via `taiki-e/install-action` (e.g. `cargo-llvm-cov`).82- Coverage targets: critical business logic 100%, public API 90%+, general code 80%+, generated/FFI83 excluded.8485## 6. Shared guardrails8687- **`?` over `unwrap()`** in all library/production code; `expect("invariant")` only for the truly88 unreachable.89- **Library → `thiserror`, application → `anyhow`**; never `Box<dyn Error>` in a library; state any90 change to the error strategy.91- Make illegal states unrepresentable; match exhaustively; newtype your primitives.92- Keep `unsafe` minimal and documented with a `# Safety` comment.93- Expose the narrowest `pub` surface; organize by domain.94- **TDD**: write the failing test first; tests are independent, never `sleep()`, and assert on95 typed error *variants* (`matches!`) rather than panic strings wherever a `Result` is returned.96- Gate merges on `fmt --check`, `clippy -D warnings`, `cargo test`, and the coverage threshold.