# Rust Style

> General Rust conventions for Pluto. Use it once for understanding the codebase better.

- Skill: `nethermindeth/rust-style` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nethermindeth/rust-style`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nethermindeth/rust-style/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: nethermindeth (https://skillmd.com/u/nethermindeth)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nethermindeth/rust-style

---


## Quality Gate

Run from `pluto/` before declaring any work done:

```bash
cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
cargo deny check --hide-inclusion-graph
```

All must pass clean.

---

## Error Handling

Define module-local error enums with `thiserror`:

```rust
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("message: {0}")]
    Variant(String),

    #[error(transparent)]
    Underlying(#[from] OtherError),
}

pub type Result<T> = std::result::Result<T, Error>;
```

Rules:
- `errors.New("msg")` → enum variant with `#[error("msg")]` (match strings exactly)
- `errors.Wrap(err, "...")` → `#[from]` / `#[source]` where appropriate
- Every field/payload in an error variant **must** appear in its `#[error("...")]` format string. If a field is not surfaced in the error message, either include it or remove it from the variant. Dead payload (captured but never displayed) is not allowed.
- Always propagate with `?`; never swallow errors with `.ok()` or `filter_map` in production code
- No `unwrap()`, `expect()`, `panic!()` outside of test code
- No `anyhow` in library crates; use typed errors everywhere

---

## Arithmetic

All arithmetic must be checked — `arithmetic_side_effects = "deny"` is enforced:

```rust
// Bad
let x = a + b;

// Good
let x = a.checked_add(b).ok_or(Error::Overflow)?;
```

---

## Casts

**Never use `as` for numeric type conversions** — use fallible conversions with `try_from`:

```rust
// Bad - will cause clippy errors
let x = value as u32;
let y = some_usize as u64;

// Good - use try_from with proper error handling
let x = u32::try_from(value)?;
let y = u64::try_from(some_usize).expect("message explaining why this is safe");
```

Rules:

- Always use `TryFrom`/`try_from` for numeric conversions between different types
- Handle conversion failures explicitly (either with `?` or `expect` with justification)
- The only acceptable use of `expect` is when the conversion is guaranteed to succeed (e.g., `usize` to `u64` on 64-bit platforms)
- Clippy will error on unchecked `as` casts: `cast_possible_truncation`, `cast_possible_wrap`, `cast_sign_loss`

---

## Conversion Traits

When **implementing** a conversion, implement `TryFrom` (not `TryInto`) and `From` (not `Into`):

```rust
// Bad - implementing TryInto directly
impl TryInto<Foo> for Bar {
    type Error = Error;
    fn try_into(self) -> Result<Foo, Error> { /* ... */ }
}

// Good - implement TryFrom; the matching TryInto comes for free
impl TryFrom<Bar> for Foo {
    type Error = Error;
    fn try_from(value: Bar) -> Result<Foo, Error> { /* ... */ }
}
```

Rules:

- Implement `TryFrom<A> for B` / `From<A> for B`. The standard library's blanket impls then provide `TryInto`/`Into` automatically; the reverse is not true.
- When specifying trait **bounds** on a generic function, the preference flips: bound on `TryInto`/`Into` so callers that implement only those traits still work.

---

## Async / Tokio

- Prefer `async`/`await` for I/O and network-bound code; use Tokio as the runtime.
- In async contexts, use `tokio::fs` / `tokio::io` instead of blocking `std::fs` / `std::io`.
- If you must call blocking or CPU-heavy code from async (crypto, large serialization, filesystem walks), isolate it with `tokio::task::spawn_blocking`.
- Prefer Tokio sync primitives (`tokio::sync::*`) over `std::sync::*` when tasks may `.await`.
- Use `tokio::time` for timeouts, sleeps, and intervals (avoid `std::thread::sleep`).

---

## Code Style

- Naming: modules/functions `snake_case`, types `PascalCase`, constants `SCREAMING_SNAKE_CASE`.
- Formatting: prefer named arguments in formatting macros:
  - ✅ `format!("hello {name}")`
  - ❌ `format!("hello {}", name)`
- Documentation:
  - Prefer copying doc comments from Go and adapting to Rust conventions (avoid “Type is a …”).
  - Avoid leaving TODOs in merged code. If a short-lived internal note is necessary, use `// TODO:` and remove before PR merge.

## Imports

Import **modules, types, traits, enums, and constants** — but **not free (standalone) functions**. Call free functions qualified through their parent module so the call site shows where the function comes from.

```rust
// Bad — free function imported bare
use crate::name::peer_name;
let label = peer_name(&id);

// Good — import the module, call qualified
use crate::name;
let label = name::peer_name(&id);
```

For a function re-exported at a crate root, qualify through the crate name (already in scope) rather than adding a bare `use`:

```rust
// Bad
use pluto_k1util::load;
let key = load(path)?;

// Good
let key = pluto_k1util::load(path)?;
```

Rules:

- Types, structs, enums, and constants **should** be imported bare (`use foo::Bar;`, then `Bar`).
- Traits **must** be imported bare — they need to be in scope for method resolution.
- Only free functions are qualified through their module. When a `use` mixes types and a free function from the same module, add `self` and drop the function: `use foo::bar::{self, SomeType};`, then call `bar::the_fn()`.
- This mirrors how helpers such as `peer_name`, `hash_proto`, and `to_0x_hex` are called across the workspace (`module::func()`), keeping call sites self-documenting.

## Generalized Parameter Types

Prefer generic parameters over concrete types when a function only needs the behavior of a trait. This mirrors the standard library's own conventions and makes functions callable with a wider range of inputs without extra allocations. Apply this especially to **public APIs**, and keep it consistent across sibling functions in the same module — a module should not mix `&str` and `impl AsRef<str>` for the same kind of argument. Full uniformity is not the goal: private helpers and hot internal paths may keep concrete types.

| Instead of | Prefer | Accepts |
| --- | --- | --- |
| `&str` | `impl AsRef<str>` | `&str`, `String`, `&String`, … |
| `&Path` | `impl AsRef<Path>` | `&str`, `String`, `PathBuf`, `&Path`, … |
| `&[u8]` | `impl AsRef<[u8]>` | `&[u8]`, `Vec<u8>`, arrays, … |
| `&Vec<T>` | `impl AsRef<[T]>` | `Vec<T>`, slices, arrays, … |
| `String` (owned, read-only) | `impl Into<String>` | `&str`, `String`, … |
| `&[T]` / iterator | `impl IntoIterator<Item = T>` | `Vec<T>`, arrays, iterators, … |

Examples:

```rust
// accepts &str, String, PathBuf, &Path, …
fn read_file(path: impl AsRef<std::path::Path>) -> std::io::Result<String> {
    std::fs::read_to_string(path.as_ref())
}

// accepts &str, String, &String, …
fn print_message(msg: impl AsRef<str>) {
    println!(“{}”, msg.as_ref());
}

// accepts &[u8], Vec<u8>, arrays, …
fn hash_bytes(data: impl AsRef<[u8]>) -> [u8; 32] {
    sha256(data.as_ref())
}
```

Rules:

- Call `.as_ref()` once at the top of the function and bind it to a local variable when the value is used in multiple places.
- Do not use `impl AsRef<T>` if the function immediately converts to an owned type anyway — use `impl Into<T>` (or just accept the owned type) in that case.
- Applies to public and private functions alike; the gain is ergonomics, not just API surface.

## Testing

- Translate Go tests to Rust where applicable; keep similar test names for cross-reference.
- Reuse the same fixtures/golden files when possible.
- Use `#[tokio::test]` for async tests.
- Use `test-case` for repeated/parameterized tests (including async):

```rust
#[cfg(test)]
mod tests {
    use test_case::test_case;

    #[test_case(1, 2 ; "small")]
    #[test_case(10, 20 ; "large")]
    fn adds(a: u64, b: u64) {
        let _ = (a, b);
    }

    #[test_case("a" ; "case_a")]
    #[test_case("b" ; "case_b")]
    #[tokio::test]
    async fn async_cases(input: &str) {
        let _ = input;
    }
}
```

- For hashing/serialization parity, generate Go-derived test vectors and hardcode them as Rust fixtures.

---

## Pluto-Specific Checklist

Apply when reviewing or porting code:

- [ ] `Ordering::SeqCst` is justified; prefer `Relaxed`/`AcqRel` for
      standalone flags.
- [ ] `Error::Io` wraps `std::io::Error` (not `String`) to preserve
      `ErrorKind`.
- [ ] New public functions accept `impl AsRef<[u8]>` / `impl AsRef<str>`
      rather than concrete slice refs where appropriate.
- [ ] No `unwrap()` / `expect()` / `panic!()` outside test code.
- [ ] All arithmetic uses checked ops (`checked_add`, `checked_mul`, …).
- [ ] Tests mirror the Go test names and shapes where applicable.
- [ ] `use` declarations appear before all other items in each file.
- [ ] No dead payload in error variants (every captured field appears in the
      `#[error("...")]` string).
- [ ] Conversions implement `TryFrom`/`From` rather than `TryInto`/`Into`
      (not caught by Clippy for `TryInto`).

