Rust Development

D5.0

This skill enforces new code or code changes to conform to proper Rust guidelines.

intermediateCoding & Development- rust
Get This Skill on GitHub

Overview


name: rust-development description: Expert Rust development guidance covering naming conventions, type safety, error handling, documentation, and best practices based on official Rust guidelines. license: Apache-2.0 tags:

  • rust
  • development
  • best-practices
  • coding-standards
  • type-safety

Rust Development

This skill enforces new code or code changes to conform to proper Rust guidelines.

When to Use

Activate this skill when:

  • Writing new Rust code
  • Reviewing or modifying existing Rust code
  • Designing Rust APIs, libraries, or applications
  • Refactoring Rust codebases

Required before submitting code:

  • All code must pass cargo clippy -- -D warnings
  • All code must be formatted with cargo +nightly fmt

Guidelines Sources

These guidelines are based on:


Naming Conventions (RFC 430)

Case Rules

ItemConventionExample
Modulessnake_casemy_module
Types & TraitsUpperCamelCaseMyStruct, MyTrait
Enum variantsUpperCamelCaseSome, None
Functions & Methodssnake_casedo_something
Macrossnake_case!my_macro!
Statics & ConstantsSCREAMING_SNAKE_CASEMAX_SIZE
Type parametersSingle uppercase letterT, U, E
LifetimesSingle lowercase letter with ''a, 'b

Important Rules

  • Acronyms count as one word: use Uuid, not UUID; HttpRequest, not HTTPRequest
  • In snake_case, single-letter words only appear last: btree_map, not b_tree_map
  • Avoid -rs or -rust suffixes in crate names
  • Feature names should be direct: use std, not use-std

Conversion Methods

PrefixCostOwnershipExample
as_Freeborrowed → borrowedas_str(), as_bytes()
to_Expensiveborrowed → ownedto_string(), to_vec()
into_Variableowned → owned (non-Copy)into_inner(), into_vec()

The mut qualifier goes where it would in the return type: as_mut_slice(), not as_slice_mut().

Getter Methods

  • Omit the get_ prefix: use first(), not get_first()
  • Reserve get for single, obvious values like Cell::get

Iterator Methods

Collections should provide:

fn iter(&self) -> Iter<'_, T>       // borrowed iteration
fn iter_mut(&mut self) -> IterMut<'_, T>  // mutable iteration
fn into_iter(self) -> IntoIter<T>  // owned iteration

Iterator type names must match their producing methods.


Type Safety

Use Newtypes for Distinctions

Create wrapper types to distinguish values with the same underlying type:

// Bad: easy to confuse
fn set_dimensions(width: u32, height: u32) { }

// Good: compiler catches mistakes
struct Width(u32);
struct Height(u32);
fn set_dimensions(width: Width, height: Height) { }

Avoid Primitive Obsession

Use custom types instead of bool, u8, or Option when meaning is unclear:

// Bad: what do these bools mean?
Widget::new(true, false)

// Good: self-documenting
enum Size { Small, Large }
enum Shape { Round, Square }
Widget::new(Size::Small, Shape::Round)

Bitflags for Multiple Flags

Use the bitflags crate for independent flags:

use bitflags::bitflags;

bitflags! {
    struct Permissions: u32 {
        const READ = 0b001;
        const WRITE = 0b010;
        const EXECUTE = 0b100;
    }
}

Builders for Complex Construction

For types with many optional parameters:

struct Config {
    timeout: Duration,
    retries: u32,
    // ...
}

struct ConfigBuilder { /* ... */ }

impl ConfigBuilder {
    fn new() -> Self { /* ... */ }
    fn timeout(mut self, timeout: Duration) -> Self { /* ... */ }
    fn retries(mut self, retries: u32) -> Self { /* ... */ }
    fn build(self) -> Result<Config, Error> { /* ... */ }
}

Trait Implementations

Common Traits to Implement

Types should eagerly implement applicable standard traits:

TraitWhen to Implement
CloneAlmost always
CopyFor small, trivially copyable types
DebugAlways for public types
DefaultWhen a sensible default exists
PartialEq, EqFor comparable types
PartialOrd, OrdFor orderable types
HashFor types used as map keys
DisplayFor user-facing output
Send, SyncWhen thread-safe

Conversion Traits

// Implement From for infallible conversions
impl From<&str> for MyString { }

// Implement TryFrom for fallible conversions
impl TryFrom<&str> for MyValidatedString {
    type Error = ValidationError;
}

// Implement AsRef/AsMut for borrowing
impl AsRef<str> for MyString { }

// Never implement Into/TryInto directly - they're derived automatically

Collection Traits

Collections should implement:

  • FromIterator - enables collect()
  • Extend - enables extend()
  • IntoIterator - enables for loops

Serialization

Implement Serde traits behind a feature flag:

[features]
serde = ["dep:serde"]

[dependencies]
serde = { version = "1", features = ["derive"], optional = true }
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MyType { }

Object Safety for Trait Objects

If a trait may be useful as a trait object (dyn Trait), ensure it's object-safe:

// Object-safe: can be used as dyn Trait
trait Drawable {
    fn draw(&self);
    fn bounds(&self) -> Rect;
}

// NOT object-safe: has generic method
trait Processor {
    fn process<T>(&self, item: T);  // Generics not allowed
}

// NOT object-safe: returns Self
trait Clonable {
    fn clone(&self) -> Self;  // Self by value not allowed
}

Only Smart Pointers Implement Deref

Only implement Deref and DerefMut for smart pointer types:

// Good: Smart pointer implementing Deref
struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T { &self.0 }
}

// Bad: Non-pointer type implementing Deref
struct Email(String);
impl Deref for Email {
    type Target = str;  // Don't do this - use AsRef instead
    fn deref(&self) -> &str { &self.0 }
}

Use AsRef instead for non-pointer types that provide access to inner data.

Debug Output Should Never Be Empty

Debug implementations should always produce meaningful output:

// Bad: Empty debug output
impl fmt::Debug for MyType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        Ok(())  // Produces nothing
    }
}

// Good: At minimum show type name
impl fmt::Debug for MyType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("MyType").finish()
    }
}

Error Handling

Error Type Requirements

Error types must:

  • Implement std::error::Error
  • Implement Send + Sync
  • Implement Display with lowercase message, no trailing punctuation
  • Never use () as an error type
use std::error::Error;
use std::fmt;

#[derive(Debug)]
pub struct MyError {
    kind: ErrorKind,
    source: Option<Box<dyn Error + Send + Sync>>,
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "operation failed: {}", self.kind)  // lowercase, no period
    }
}

impl Error for MyError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        self.source.as_ref().map(|e| e.as_ref() as _)
    }
}

Use thiserror for Library Errors

use thiserror::Error;

#[derive(Debug, Error)]
pub enum MyError {
    #[error("failed to read file: {path}")]
    ReadFile { path: String, source: std::io::Error },

    #[error("invalid configuration")]
    InvalidConfig,
}

Use anyhow for Application Errors

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let config = read_config()
        .context("failed to read configuration")?;
    Ok(())
}

Propagate with ?, Not unwrap()

// Bad
let file = File::open(path).unwrap();

// Good
let file = File::open(path)?;

// Good with context
let file = File::open(path)
    .with_context(|| format!("failed to open {}", path.display()))?;

Use let ... else for Early Returns

Extract values and exit early to keep the happy path unindented:

// Bad: Nested conditionals
fn process(value: Option<String>) -> Result<(), Error> {
    if let Some(s) = value {
        if !s.is_empty() {
            // happy path buried in nesting
            do_work(&s)?;
        }
    }
    Ok(())
}

// Good: let-else keeps happy path flat
fn process(value: Option<String>) -> Result<(), Error> {
    let Some(s) = value else {
        return Ok(());
    };

    if s.is_empty() {
        return Ok(());
    }

    do_work(&s)?;
    Ok(())
}

Benefits:

  • Keeps the main logic at the top level of indentation
  • Makes early exit conditions explicit
  • Reduces rightward drift in complex functions

Documentation

Requirements

  • Every public item must have documentation
  • Crate-level docs (//! in lib.rs) should provide overview and examples
  • Include examples that compile and run

Function Documentation Sections

/// Brief description of what this function does.
///
/// More detailed explanation if needed.
///
/// # Arguments
///
/// * `arg1` - Description of first argument
///
/// # Returns
///
/// Description of return value.
///
/// # Errors
///
/// Returns `Err` if:
/// - condition one occurs
/// - condition two occurs
///
/// # Panics
///
/// Panics if:
/// - some invariant is violated
///
/// # Safety
///
/// This function is unsafe because:
/// - caller must ensure X
///
/// # Examples
///
/// ```
/// use my_crate::my_function;
///
/// let result = my_function(42)?;
/// assert_eq!(result, 84);
/// # Ok::<(), my_crate::Error>(())
/// ```
pub fn my_function(arg1: i32) -> Result<i32, Error> { }

Example Guidelines

  • Use ? for error handling, not unwrap() or try!
  • Hide boilerplate with # prefix
  • Ensure examples compile with cargo test --doc

Hide Implementation Details

Use #[doc(hidden)] to hide internal items from documentation:

// Public but hidden from docs
#[doc(hidden)]
pub mod __internal {
    // Implementation details needed by macros
}

// Hide re-exports of internal types
#[doc(hidden)]
pub use crate::internal::Helper;

Don't expose helper types, internal modules, or implementation details in public documentation.


Memory and Safety

Minimize unsafe

  • Avoid unsafe when safe alternatives exist
  • Isolate unsafe code in small, well-documented functions
  • Document all safety invariants
/// Returns a reference to the element at `index`.
///
/// # Safety
///
/// Caller must ensure `index < self.len()`.
pub unsafe fn get_unchecked(&self, index: usize) -> &T {
    // SAFETY: caller guarantees index is in bounds
    unsafe { self.data.get_unchecked(index) }
}

Prefer Owned Types for Thread Safety

  • Return owned types when the data might be used across threads
  • Document Send and Sync bounds explicitly

Avoid Interior Mutability Unless Necessary

  • Prefer &mut self methods over Cell/RefCell
  • When using interior mutability, document why it's needed

Performance

Avoid Unnecessary Allocations

// Bad: allocates unnecessarily
fn greet(name: &str) -> String {
    format!("Hello, {}!", name.to_string())
}

// Good: reuses the input
fn greet(name: &str) -> String {
    format!("Hello, {name}!")
}

Use Iterators Over Indexing

// Bad: bounds checking on each access
for i in 0..vec.len() {
    process(vec[i]);
}

// Good: optimized iteration
for item in &vec {
    process(item);
}

Prefer &str Over String in Parameters

// Bad: forces caller to allocate
fn process(s: String) { }

// Good: accepts both String and &str
fn process(s: &str) { }

// Good: generic over string types
fn process(s: impl AsRef<str>) { }

Use Cow for Conditional Ownership

use std::borrow::Cow;

fn process(input: &str) -> Cow<'_, str> {
    if needs_modification(input) {
        Cow::Owned(modify(input))
    } else {
        Cow::Borrowed(input)
    }
}

API Design

Functions Should Not Take Out-Parameters

// Bad: out-parameter
fn get_values(output: &mut Vec<i32>) { }

// Good: return the value
fn get_values() -> Vec<i32> { }

Expose Intermediate Results

Allow callers to avoid duplicate work:

// Bad: forces re-parsing
fn is_valid(s: &str) -> bool { }
fn parse(s: &str) -> Result<Value, Error> { }

// Good: parse once, check validity from result
fn parse(s: &str) -> Result<Value, Error> { }
// Caller can check is_ok() for validity

Constructors Are Static Methods

impl MyType {
    // Primary constructor
    pub fn new() -> Self { }

    // Alternative constructors
    pub fn with_capacity(cap: usize) -> Self { }
    pub fn from_parts(a: A, b: B) -> Self { }
}

Smart Pointers Don't Add Inherent Methods

Types implementing Deref should not add methods that might conflict with the target type.

Use Generics to Minimize Assumptions

Accept generic parameters instead of concrete types when possible:

// Bad: Requires specific type
fn read_config(file: File) -> Config { }

// Good: Accepts any reader
fn read_config(reader: impl Read) -> Config { }

// Good: Accepts any string-like type
fn greet(name: impl AsRef<str>) {
    println!("Hello, {}", name.as_ref());
}

Validate Function Arguments

Functions should validate their inputs rather than trusting callers:

// Bad: Trusts caller to validate
fn get_element(slice: &[u8], index: usize) -> u8 {
    slice[index]  // Panics on invalid index
}

// Good: Returns Option for invalid input
fn get_element(slice: &[u8], index: usize) -> Option<u8> {
    slice.get(index).copied()
}

// Good: Use types that enforce validity
fn process_nonempty(items: NonEmpty<Item>) { }

Project Structure

Cargo.toml Metadata

[package]
name = "my-crate"
version = "0.1.0"
edition = "2021"
rust-version = "1.70"  # MSRV
authors = ["Author Name <email@example.com>"]
description = "Brief description of the crate"
documentation = "https://docs.rs/my-crate"
repository = "https://github.com/user/my-crate"
license = "MIT OR Apache-2.0"
keywords = ["keyword1", "keyword2"]
categories = ["category1"]

[dependencies]
# Use specific versions or version ranges
serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
# Test-only dependencies

Module Organization

src/
├── lib.rs          # Public API, re-exports
├── error.rs        # Error types
├── types.rs        # Core types
├── internal/       # Private implementation
│   ├── mod.rs
│   └── helpers.rs
└── tests/          # Integration test helpers

Feature Flags

  • Use feature flags for optional functionality
  • Name features clearly without placeholder words
  • Document features in Cargo.toml and README
[features]
default = []
serde = ["dep:serde"]
async = ["dep:tokio"]

Testing

Test Organization

// Unit tests in the same file
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_basic_functionality() {
        // ...
    }
}

// Integration tests in tests/ directory
// tests/integration_test.rs

Test Naming

#[test]
fn function_name_condition_expected_result() {
    // e.g., parse_valid_input_returns_value
}

Property-Based Testing

Consider using proptest or quickcheck for property-based tests:

use proptest::prelude::*;

proptest! {
    #[test]
    fn roundtrip_serialization(value: MyType) {
        let serialized = serialize(&value);
        let deserialized = deserialize(&serialized)?;
        prop_assert_eq!(value, deserialized);
    }
}

Future Proofing

Use Private Fields

Structs should have private fields to allow future changes without breaking users:

// Bad: Public fields lock in representation
pub struct Point {
    pub x: f64,
    pub y: f64,
}

// Good: Private fields with accessors
pub struct Point {
    x: f64,
    y: f64,
}

impl Point {
    pub fn new(x: f64, y: f64) -> Self { Self { x, y } }
    pub fn x(&self) -> f64 { self.x }
    pub fn y(&self) -> f64 { self.y }
}

Sealed Traits

Use sealed traits to prevent downstream implementations while allowing use:

mod private {
    pub trait Sealed {}
}

/// This trait is sealed and cannot be implemented outside this crate.
pub trait MyTrait: private::Sealed {
    fn method(&self);
}

// Only types in this crate can implement MyTrait
impl private::Sealed for MyType {}
impl MyTrait for MyType {
    fn method(&self) { }
}

Dependability

Destructors Must Not Fail

Drop implementations must not panic or fail:

impl Drop for Resource {
    fn drop(&mut self) {
        // Bad: Can panic
        self.file.flush().unwrap();

        // Good: Log error but don't panic
        if let Err(e) = self.file.flush() {
            eprintln!("warning: failed to flush: {e}");
        }
    }
}

If cleanup can fail, provide an explicit close() method that returns Result.


Clippy and Formatting

Required Clippy Lints

Enable these in lib.rs or main.rs:

#![warn(
    clippy::all,
    clippy::pedantic,
    clippy::nursery,
    missing_docs,
    rust_2018_idioms,
)]

#![allow(
    clippy::module_name_repetitions,  // if needed
)]

Rustfmt Configuration

Create rustfmt.toml:

edition = "2021"
max_width = 100
use_small_heuristics = "Default"

Required: Run Before Committing

All code must pass these checks before being submitted:

# Format code (required)
cargo +nightly fmt

# Check for lint errors (required - must pass with no warnings)
cargo clippy -- -D warnings

# Run tests
cargo test

# Verify docs build
cargo doc --no-deps

Code that fails clippy or is not properly formatted should not be committed.


Checklist for Code Review

Required

  • Code passes cargo clippy -- -D warnings
  • Code is formatted with cargo +nightly fmt
  • Tests pass with cargo test

Naming

  • Follows RFC 430 case conventions
  • Conversions use as_/to_/into_ correctly
  • No get_ prefix on simple getters
  • Iterator methods follow conventions

Types

  • Uses newtypes for distinct concepts
  • Avoids bool/Option parameters with unclear meaning
  • Implements appropriate standard traits
  • Error types implement Error + Send + Sync

Safety

  • Minimizes unsafe code
  • Documents safety invariants
  • Validates inputs at boundaries

Documentation

  • All public items documented
  • Examples compile and demonstrate usage
  • Error/panic conditions documented

Performance

  • Avoids unnecessary allocations
  • Uses iterators appropriately
  • Takes references where ownership isn't needed

Testing

  • Unit tests for core logic
  • Integration tests for public API
  • Edge cases covered

What This Skill Can Do

AI-generated examples showing real capabilities

Ready to use this skill?

Visit the original repository to get the full skill configuration and installation instructions.

View on GitHub

Related Skills