Skip to main content

rust-best-practices

Idiomatic Rust guidance adapted from BeautifulRust for KMS; use when writing, reviewing or refactoring Rust.

Source facts

Repository
cosmian/kms
Last source activity
November 6, 2026 at 19:39
Detected SKILL.md language
English
Stars
372
Forks
43

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
3 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
rust-best-practices
description
Idiomatic Rust guidance adapted from BeautifulRust for KMS; use when writing, reviewing or refactoring Rust.
# Rust best practices Adapted from [BeautifulRust rust-best-practices](https://github.com/morganlinton/BeautifulRust/blob/main/skills/rust-best-practices/SKILL.md), MIT, see `LICENSE`. This is the generic idiom baseline for Rust in this repository. It never loosens a KMS rule. These stay normative in their own files and win on any conflict: - Lints, suppressions, panics, tests, feature placement: [`rust.instructions.md`](../../instructions/rust.instructions.md) (§ Clippy and lint suppression, § Feature-flag discipline). - Secrets, constant-time comparison, log hygiene: [`rust-security.instructions.md`](../../instructions/rust-security.instructions.md). - Crypto, FIPS gating, randomness (`KmsRng` only): [`rust-crypto.instructions.md`](../../instructions/rust-crypto.instructions.md) and `rust.instructions.md` § Randomness. - Server, database, KMIP structure: [`rust-server`](../../instructions/rust-server.instructions.md), [`rust-database`](../../instructions/rust-database.instructions.md), [`rust-kmip`](../../instructions/rust-kmip.instructions.md). KMS-specific design patterns (non-normative): [`references/kms-patterns.md`](references/kms-patterns.md). Prefer the smallest change that fixes the request. Guidance below is a review lens, not a mandate to rewrite working code: every item is subject to compatibility (wire format, CLI/TOML/env names, REST JSON, public API) and security. ## 1. Mindset - **Write Rust-shaped code.** Model errors with `Result`, resources with ownership and `Drop`, polymorphism with traits and enums, nullability with `Option`. Inheritance-flavored tricks (`Deref` to a "base" struct, `Manager`/`Factory` types, `throw_if_null` helpers) are smells. - **Make invalid states unrepresentable.** Push invariants into types: enums instead of bool flags and "must be X if Y" comments, newtypes for units and IDs, fallible constructors that validate once. - **Application code is not library code.** Code with external users follows the API-design rules below. Code internal to a binary favors simplicity: concrete types, `&str`/`String` parameters, no speculative generality. Keep each crate's existing error type and its `*_error!` / `*_bail!` macros (§5). - **Let the tools do the work.** rustfmt owns formatting, clippy owns idiom checks, the compiler owns memory safety. Spend attention on design. ## 2. Before you hand back code Run, scoped to what you touched, and fix everything reported: ```bash cargo fmt --all cargo clippy-all cargo clippy --workspace --all-targets -- -D warnings # FIPS default cargo test -p <crate> [<test_name>] cargo test-fips # full FIPS workspace suite cargo test-non-fips # full non-FIPS workspace suite ``` - Fix a lint rather than silencing it. Suppression rules live in [`rust.instructions.md` § Clippy and lint suppression](../../instructions/rust.instructions.md#clippy-and-lint-suppression--zero-new-allow--expect-policy); do not restate or extend them here. - Never put `#![deny(warnings)]` / `#![allow(warnings)]` in source. Lint levels live in the root `Cargo.toml` (`[workspace.lints]`) and `clippy.toml`. ## 3. Ownership and borrowing **Prefer unique ownership.** Reach for `Rc`/`Arc` only when shared ownership is part of the design, and `RefCell`/`Mutex` only when interior mutability is. They move checking from compile time to run time, so use them on purpose. `Arc<Mutex<T>>` around data that is never mutated is a smell. **Don't clone to make the borrow checker stop complaining.** A clone that exists only to end a borrow produces a silent copy. Try first, roughly in order: 1. Extend a temporary with a named `let`, or shorten a borrow with a block. 2. Split a long expression into typed intermediates so the failing step is visible. 3. Move a field out of `&mut self` with `std::mem::take`, `std::mem::replace`, or `Option::take`. 4. Split a large struct so disjoint fields can be borrowed separately. 5. Clone small, immutable data deliberately, with a comment when not obvious. 6. Indices into an owning `Vec`, or `Rc<RefCell<_>>` with `Weak` back-pointers, as a last resort. Avoid self-referential structs. ```rust // Move out of a &mut enum without cloning. fn a_to_b(e: &mut MyEnum) { if let MyEnum::A { name, x: 0 } = e { *e = MyEnum::B { name: std::mem::take(name) }; } } ``` **Accept the most general borrowed type.** `&str` not `&String`, `&[T]` not `&Vec<T>`, `&T` not `&Box<T>`, `Option<&T>` not `&Option<T>`. `impl AsRef<Path>` / `impl AsRef<str>` / `impl AsRef<[u8]>` is fine on parameters that only read (never on struct fields). Code that only iterates may take `impl IntoIterator<Item = T>`. **Take ownership only when you need it, and then by value.** If a function stores or consumes its argument, take `T`, not `&T` plus an internal `.clone()`. Return multiple values as a tuple or struct, not through `&mut` out-parameters (reusing a caller-owned buffer is the exception). **Prefer owned data in structs.** A struct holding `&'a T` forces a lifetime onto every container. When a type does carry a lifetime, give it a descriptive name (`'src`, `'conn`). **Keep `Rc`, `Arc`, `Box` and `RefCell` out of public signatures** where practical: take `&T`, `&mut T` or `T`. A service that must be shared wraps its state in `Arc<Inner>` internally. Boxing a large enum variant (`clippy::large_enum_variant`) is the accepted exception. ## 4. Types and API design ### Naming | Item | Case | |---|---| | Types, traits, enum variants | `UpperCamelCase` (acronyms are one word: `Uuid`, `HttpClient`) | | Functions, methods, modules, locals | `snake_case` | | Constants, statics | `SCREAMING_SNAKE_CASE` | - Conversion prefixes signal cost: `as_` (free borrow), `to_` (expensive), `into_` (consumes `self`). Constructors: `new`, `with_*` variants, `try_new` (fallible), `from_*` (pair important ones with `From`). - Getter naming (`get_` prefix or not) follows the surrounding module; renaming existing getters is churn, not a fix. - Avoid weasel suffixes `Manager`, `Factory`, `Helper`, `Util` in new names: name the thing it is. If you implement `len()`, implement `is_empty()`. - Error type names read verb-object-error: `ParseBoolError`. ### Model the domain with types - **Newtypes** for semantic distinctions (`struct Miles(f64)`, identifiers) where mixing values is a real risk. A newtype that encodes an invariant validates in a fallible constructor (`TryFrom`, `FromStr`, `try_new`) and offers no infallible `From` from the raw type. - **No `bool` parameters in public or shared signatures.** `report(true)` is unreadable and cannot grow a third mode: use a two-variant enum or two methods. A private helper whose call sites sit a few lines away is exempt. Several independent flags in one DTO that mirror CLI/TOML/JSON options are governed by `clippy.toml` `max-struct-bools`. - **Named fields over tuples**, except single-field newtypes and very local code. - **`Option` for absence, never sentinels** such as `-1` or `""`. **Data-carrying enums** instead of "field X is only meaningful when Y is set". - **Strongest std type:** `PathBuf`/`&Path` for paths, `Duration` for spans. Keep plain integers at API boundaries. - A `u32` conversion from a fieldless enum is `u32::from(value)` (generated by `kmip_enum` for KMIP enums), not `value as u32`. ### Standard traits Derive what every field supports, when it is useful: `Debug`, `Clone`, `Copy` (small plain types only), `PartialEq`/`Eq`, `PartialOrd`/`Ord`, `Hash` (consistent with `Eq`), `Default`. Exceptions: **secrets and key material** are never given a derived `Debug`/`Clone`; hand-write `Debug` to redact them and keep them in `Zeroizing` ([`rust-security.instructions.md`](../../instructions/rust-security.instructions.md)). Implement `From`/`TryFrom`, never `Into`/`TryInto`. Prefer `from`/`into` over `as`, which silently allows lossy casts. Implement `FromStr` rather than an ad-hoc `from_str`. Implement `Deref` only on smart pointers and owning collections. If you hand-write `Ord`, hand-write `PartialEq`, `PartialOrd` and `Hash` to agree with it. Don't put derivable bounds on struct definitions; bounds belong on the `impl` blocks that need them. ### Constructors, builders, defaults - A constructor with four or more loose arguments groups them into semantic types or a parameter struct (`Deposit::new(account, amount)`); that is how `clippy::too_many_arguments` is fixed here. A builder is one option when construction validates or has many optionals; it is not mandatory. - If you provide both `new()` and `Default`, make them agree. ### Visibility and evolution - Private by default; widen to `pub(crate)`/`pub(super)` before `pub`. Every `pub` item is a promise. - `#[non_exhaustive]` and sealed traits are contextual choices for published API whose evolution you must protect. The KMS crates are not consumed by third parties, so do not add them automatically. - Expose each public item at one path; do not `pub use` foreign crates' items without a reason; avoid leaking third-party types into new public APIs. - Mark a function `const` only if it will stay `const`. ### Traits and generics - Define a trait for generic programming (`T: Trait`, `impl Trait`, `dyn Trait`) or as an extension trait, not to organize code. Put essential functionality in inherent methods. - Prefer, in order: concrete types; generics; `dyn Trait` (the database/HSM backends are the legitimate `dyn` boundaries). Don't nest generics on service-level types. - If a function has a clear receiver, make it a method. A computation without a receiver is a free function. - Closures: use the most general bound that works (`FnOnce`, `FnMut`, then `Fn`); closure parameters go last. - Reach for a macro only when functions, generics and traits cannot do the job. Prefer `macro_rules!` to proc macros. ### Refactoring someone else's code - Fix every smell the request is about, not just the cheap ones: clones that only satisfy the borrow checker, `&String`/`&Vec<T>` parameters, `unwrap` on fallible paths, `bool` parameters, `+` string concatenation, nondeterministic `HashMap` iteration in output. - Keep public behavior identical unless changing it is the point: same operations, same success and failure semantics, same output bytes. Turning a panic into an `Err` is the change callers want; dropping an error variant or changing a constructor's inputs is not. - Preserve quirks you don't understand and flag them rather than silently "fixing" them. - When the code has no tests, pin the current behavior with a few before you touch it (unit tests live in a same-file `#[cfg(test)]` submodule). ## 5. Errors and panics | Situation | Use | |---|---| | Absence is normal (`first()`, `checked_add`) | `Option<T>` | | The caller may need to know what went wrong | `Result<T, E>` with the crate's error type | | A bug or violated invariant inside your code | a typed error; **not** a production panic | **Production code never panics.** No `unwrap`, `expect`, `panic!`, `todo!`, `unimplemented!`, `unreachable!` (outside `debug_assertions`), `process::exit`, unchecked arithmetic/indexing on untrusted data: the full list and allowed exceptions are in [`rust.instructions.md` § Panics and brutal exits](../../instructions/rust.instructions.md#panics-and-brutal-exits--forbidden-in-production-code). An invariant that "cannot happen" is still returned as an error variant. Tests may use a meaningful `.expect("<why this cannot fail>")`. **Pattern-match instead of check-then-unwrap:** `if let Some(v) = opt { .. }` or `let Some(v) = opt else { return .. };`. **Lock poisoning** is mapped to the crate's existing error (e.g. a `KmsError`/`DbError` variant) with `?`/`map_err`. Neither `.expect("lock poisoned")` nor silent recovery with `into_inner`. **Error types:** keep each crate's existing `thiserror` error type and its `*_error!` / `*_bail!` macros (`kms_error!`/`kms_bail!`, `db_error!`/`db_bail!`, …). Do not introduce `anyhow`/`eyre`, a second error type per crate, or `Box<dyn Error>`/`String` as a public error type. Error types implement `std::error::Error + Send + Sync + 'static`; `Display` messages are concise; implement `From<Upstream>` so `?` converts and use `map_err` only for foreign errors or to add context. Error messages never contain secrets. **Destructors never fail, panic, or block.** Provide `close(self) -> Result<()>` for fallible teardown. **Document failure modes:** public items document applicable `# Errors`, `# Panics` and `# Safety`. ## 6. Everyday idioms - **Iterators over hand loops** when the pipeline is short and reads top-down; collect fallible steps into `Result<Vec<_>, E>` and apply `?`. Keep an explicit `for` loop when the body is large or needs awkward early returns. - **Option/Result combinators** (`map`, `and_then`, `ok_or_else`, `as_deref`) when clearer than `match`; `match` when they're not. Use guard clauses, `let-else` and `?` to flatten nesting. - **`format!`/`write!`** with inline args (`format!("{name}: {count}")`), not chained `+`. - **Explicit types where inference obscures intent:** `.into()`, `.parse()`, `.collect()`. - **Drop guards:** bind to a name (`let _guard = ..`); `let _ = ..` drops immediately. - **Imports:** one `use` per module path, at the top of the file (never inside function bodies); no globs except `use super::*` in tests and `use Enum::*` in a function that matches on it. - **Magic numbers** become named `const`s with a doc comment. - **Logging:** structured events through `tracing`, not `println!`/`dbg!`, except a CLI's own stdout output. Never log secrets. - Use `Self` inside `impl` blocks; prefer `impl Trait` in argument position when the bound is used once. - Functions over 60 lines or nesting of five or more levels are readability-review candidates, not failures; do not extract helpers automatically. ## 7. Concurrency and async - Public types, and the futures they return, should be `Send`. Holding an `Rc` or a `RefCell` borrow across an `.await` makes a future `!Send`. - Prefer message passing to shared state. When sharing: keep data with linked invariants under one lock, keep lock scopes to a few lines, never call user closures while holding a lock, never return a `MutexGuard` from a public method, never hold a lock across `.await` unless it is an async lock by design. - Write `async fn` rather than `fn -> impl Future`, except in traits or when setup must run outside the async block. - CPU-bound or blocking work inside async moves to `spawn_blocking` (see `/rust-async-refactor`). - Library code does not spawn raw threads or assume a runtime.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub