rust-key-paths 1.11.5

Keypaths for Rust: Static dispatch implementation (rust-keypaths) and legacy dynamic dispatch (key-paths-core). Type-safe, composable access to nested data structures.
Documentation

🔑 KeyPaths & CasePaths in Rust

Key paths and case paths provide a safe, composable way to access and modify nested data in Rust. Inspired by Swift's KeyPath / CasePath system, this feature rich crate lets you work with struct fields and enum variants as first-class values.


🚀 New: Static Dispatch Implementation

We now provide two implementations:

Primary: rust-keypaths + keypaths-proc (Recommended)

  • Static dispatch - Faster performance, better compiler optimizations
  • Write operations can be faster than manual unwrapping at deeper nesting levels
  • Zero runtime overhead - No dynamic dispatch costs
  • Better inlining - Compiler can optimize more aggressively
[dependencies]
rust-keypaths = "1.0.2"
keypaths-proc = "1.0.1"

Legacy: key-paths-core + key-paths-derive (v1.6.0)

  • ⚠️ Dynamic dispatch - Use only if you need:
    • Send + Sync bounds for multithreaded scenarios
    • Dynamic dispatch with trait objects
    • Compatibility with existing code using the enum-based API
[dependencies]
key-paths-core = "1.6.0"  # Use 1.6.0 for dynamic dispatch
key-paths-derive = "1.1.0"

✨ Features

  • Readable/Writable keypaths for struct fields
  • Failable keypaths for Option<T> chains (_fr/_fw)
  • Enum CasePaths (readable and writable prisms)
  • Composition across structs, options and enum cases
  • Iteration helpers over collections via keypaths
  • Proc-macros: #[derive(Keypaths)] for structs/tuple-structs and enums, #[derive(Casepaths)] for enums

📦 Installation

Recommended: Static Dispatch (rust-keypaths)

[dependencies]
rust-keypaths = "1.0.0"
keypaths-proc = "1.0.0"

Legacy: Dynamic Dispatch (key-paths-core)

[dependencies]
key-paths-core = "1.6.0"  # Use 1.6.0 for dynamic dispatch
key-paths-derive = "1.1.0"

API Differences

rust-keypaths (Static Dispatch):

use rust_keypaths::{KeyPath, OptionalKeyPath, WritableKeyPath};

let kp = KeyPath::new(|s: &Struct| &s.field);
let opt_kp = OptionalKeyPath::new(|s: &Struct| s.opt_field.as_ref());
let writable_kp = WritableKeyPath::new(|s: &mut Struct| &mut s.field);

key-paths-core (Dynamic Dispatch):

use key_paths_core::KeyPaths;

let kp = KeyPaths::readable(|s: &Struct| &s.field);
let opt_kp = KeyPaths::failable_readable(|s: &Struct| s.opt_field.as_ref());
let writable_kp = KeyPaths::writable(|s: &mut Struct| &mut s.field);

🚀 Examples

Deep Nested Composition with Box and Enums

This example demonstrates keypath composition through deeply nested structures with Box<T> and enum variants:

use keypaths_proc::{Casepaths, Keypaths};

#[derive(Debug, Keypaths)]
#[Writable]
struct SomeComplexStruct {
    scsf: Box<SomeOtherStruct>,
}

impl SomeComplexStruct {
    fn new() -> Self {
        Self {
            scsf: Box::new(SomeOtherStruct {
                sosf: OneMoreStruct {
                    omsf: String::from("no value for now"),
                    omse: SomeEnum::B(DarkStruct {
                        dsf: String::from("dark field"),
                    }),
                },
            }),
        }
    }
}

#[derive(Debug, Keypaths)]
#[Writable]
struct SomeOtherStruct {
    sosf: OneMoreStruct,
}

#[derive(Debug, Casepaths)]
#[Writable]
enum SomeEnum {
    A(String),
    B(DarkStruct),
}

#[derive(Debug, Keypaths)]
#[Writable]
struct OneMoreStruct {
    omsf: String,
    omse: SomeEnum,
}

#[derive(Debug, Keypaths)]
#[Writable]
struct DarkStruct {
    dsf: String,
}

fn main() {
    use rust_keypaths::WritableOptionalKeyPath;
    
    // Compose keypath through Box, nested structs, and enum variants
    // Using .then() method (works on stable Rust)
    let keypath = SomeComplexStruct::scsf_fw()
        .then(SomeOtherStruct::sosf_fw())
        .then(OneMoreStruct::omse_fw())
        .then(SomeEnum::b_case_fw())
        .then(DarkStruct::dsf_fw());
    
    // Alternatively, use the >> operator (requires nightly feature):
    // #![feature(impl_trait_in_assoc_type)]
    // let keypath = SomeComplexStruct::scsf_fw()
    //     >> SomeOtherStruct::sosf_fw()
    //     >> OneMoreStruct::omse_fw()
    //     >> SomeEnum::b_case_fw()
    //     >> DarkStruct::dsf_fw();
    
    let mut instance = SomeComplexStruct::new();
    
    // Mutate deeply nested field through composed keypath
    if let Some(dsf) = keypath.get_mut(&mut instance) {
        *dsf = String::from("we can update the field of struct with the other way unlocked by keypaths");
        println!("instance = {:?}", instance);
    }
}

Run it yourself:

cargo run --example box_keypath

Keypath Chaining with >> Operator

The >> operator provides a convenient syntax for chaining keypaths. It requires Rust nightly with the nightly feature enabled:

#![feature(impl_trait_in_assoc_type)]  // Must be in YOUR code
use rust_keypaths::{keypath, KeyPath};

struct User { address: Address }
struct Address { street: String }

// Create keypaths
let address_kp = keypath!(|u: &User| &u.address);
let street_kp = keypath!(|a: &Address| &a.street);

// Chain using >> operator (requires nightly feature)
let user_street_kp = address_kp >> street_kp;

// Use the chained keypath
let user = User { address: Address { street: "123 Main St".to_string() } };
println!("Street: {}", user_street_kp.get(&user));

On stable Rust, use the then() method instead:

let user_street_kp = address_kp.then(street_kp);  // Works on stable

Supported combinations:

  • KeyPath >> KeyPathKeyPath
  • KeyPath >> OptionalKeyPathOptionalKeyPath
  • OptionalKeyPath >> OptionalKeyPathOptionalKeyPath
  • WritableKeyPath >> WritableKeyPathWritableKeyPath
  • WritableKeyPath >> WritableOptionalKeyPathWritableOptionalKeyPath
  • WritableOptionalKeyPath >> WritableOptionalKeyPathWritableOptionalKeyPath

Running the example:

cargo +nightly run --example add_operator --features nightly

🌟 Showcase - Crates Using rust-key-paths

The rust-key-paths library is being used by several exciting crates in the Rust ecosystem:


🔗 Helpful Links & Resources


💡 Why use KeyPaths?

  • Avoids repetitive match / . chains.
  • Encourages compositional design.
  • Plays well with DDD (Domain-Driven Design) and Actor-based systems.
  • Useful for reflection-like behaviors in Rust (without unsafe).
  • High performance: Only 1.46x overhead for reads, 93.6x faster when reused, and essentially zero overhead for deep nested writes (10 levels)!

⚡ Performance

KeyPaths are optimized for performance with minimal overhead. Below are benchmark results comparing direct unwrap vs keypaths for 10-level deep nested access:

Operation Direct Unwrap KeyPath Overhead Notes
Read (10 levels) 384.07 ps 848.27 ps 2.21x ~464 ps absolute difference
Write (10 levels) 19.306 ns 19.338 ns 1.002x Essentially identical!

See benches/BENCHMARK_SUMMARY.md for detailed performance analysis.


🔄 Comparison with Other Lens Libraries

Limitations of lens-rs, pl-lens, and keypath

Both lens-rs, pl-lens (Plausible Labs), and keypath have several limitations when working with Rust's type system, especially for nested structures:

keypath limitations:

  1. ❌ No enum variant support: No built-in support for enum case paths (prisms)
  2. ❌ No Option chain support: Requires manual .and_then() composition for Option types
  3. ❌ Limited container support: No built-in support for Result<T, E>, Mutex<T>, RwLock<T>, or collection types
  4. ❌ No failable keypaths: Cannot easily compose through Option chains with built-in methods
  5. ❌ No writable failable keypaths: Missing support for composing writable access through Option chains
  6. ❌ Limited composition API: Less ergonomic composition compared to .then() chaining
  7. ⚠️ Maintenance status: May have limited active maintenance

pl-lens limitations:

  1. ❌ No support for Option<Struct> nested compositions: The #[derive(Lenses)] macro fails to generate proper lens types for nested structs wrapped in Option<T>, requiring manual workarounds
  2. ❌ Limited enum support: No built-in support for enum variant case paths (prisms)
  3. ❌ No automatic failable composition: Requires manual composition through .and_then() chains for Option types
  4. ❌ Limited container support: No built-in support for Result<T, E>, Mutex<T>, RwLock<T>, or collection types
  5. ❌ Named fields only: The derive macro only works with structs that have named fields, not tuple structs
  6. ❌ No writable failable keypaths: Cannot compose writable access through Option chains easily
  7. ❌ Type system limitations: The lens composition through Option types requires manual function composition, losing type safety

lens-rs limitations:

  1. ❌ Different API design: Uses a different lens abstraction that doesn't match Rust's ownership model as well
  2. ❌ Limited ecosystem: Less mature and fewer examples/documentation
  3. ❌ Composition complexity: More verbose composition syntax

Feature Comparison Table

Feature rust-keypaths keypath pl-lens lens-rs
Struct Field Access ✅ Readable/Writable ✅ Readable/Writable ✅ Readable/Writable ✅ Partial
Option Chains ✅ Built-in (_fr/_fw) ❌ Manual composition ❌ Manual composition ❌ Manual
Enum Case Paths ✅ Built-in (CasePaths) ❌ Not supported ❌ Not supported ❌ Limited
Tuple Structs ✅ Full support ⚠️ Unknown ❌ Not supported ❌ Not supported
Composition .then() chaining ⚠️ Less ergonomic ⚠️ Manual ⚠️ Complex
Result<T, E> ✅ Built-in support ❌ Not supported ❌ Not supported ❌ Not supported
Mutex/RwLock ✅ Built-in (with_mutex, etc.) ❌ Not supported ❌ Not supported ❌ Not supported
Arc/Box/Rc ✅ Built-in support ⚠️ Unknown ⚠️ Limited ⚠️ Limited
Collections ✅ Vec, HashMap, HashSet, etc. ❌ Not supported ❌ Not supported ❌ Not supported
Derive Macros #[derive(Keypaths)], #[derive(Casepaths)] #[derive(Keypath)] #[derive(Lenses)] ⚠️ Limited
Deep Nesting ✅ Works seamlessly ⚠️ May require workarounds ❌ Requires workarounds ❌ Complex
Type Safety ✅ Full compile-time checks ✅ Good ✅ Good ⚠️ Moderate
Performance ✅ Optimized (1.46x overhead reads, near-zero writes) ⚠️ Unknown ⚠️ Unknown ⚠️ Unknown
Readable Keypaths KeyPath ✅ Supported RefLens ⚠️ Partial
Writable Keypaths WritableKeyPath ✅ Supported Lens ⚠️ Partial
Failable Readable OptionalKeyPath ❌ Manual ❌ Manual ❌ Manual
Failable Writable WritableOptionalKeyPath ❌ Manual ❌ Manual ❌ Manual
Zero-cost Abstractions ✅ Static dispatch ⚠️ Unknown ⚠️ Depends ⚠️ Depends
Swift KeyPath-like API ✅ Inspired by Swift ⚠️ Partial ❌ No ❌ No
Container Methods with_mutex, with_rwlock, with_arc, etc. ❌ Not supported ❌ Not supported ❌ Not supported
Iteration Helpers iter(), iter_mut() ❌ Not supported ❌ Not supported ❌ Not supported
Derivable References ✅ Full support ✅ Full support ❌ Not supported ❌ Not supported
Active Maintenance ✅ Active ⚠️ Unknown ⚠️ Unknown ⚠️ Unknown

Key Advantages of rust-keypaths

  1. ✅ Native Option support: Built-in failable keypaths (_fr/_fw) that compose seamlessly through Option<T> chains (unlike keypath, pl-lens, and lens-rs which require manual composition)
  2. ✅ Enum CasePaths: First-class support for enum variant access (prisms) with #[derive(Casepaths)] (unique feature not found in keypath, pl-lens, or lens-rs)
  3. ✅ Container types: Built-in support for Result, Mutex, RwLock, Arc, Rc, Box, and all standard collections (comprehensive container support unmatched by alternatives)
  4. ✅ Zero-cost abstractions: Static dispatch with minimal overhead (1.46x for reads, near-zero for writes) - benchmarked and optimized
  5. ✅ Comprehensive derive macros: Automatic generation for structs (named and tuple), enums, and all container types
  6. ✅ Swift-inspired API: Familiar API for developers coming from Swift's KeyPath system with .then() composition
  7. ✅ Deep composition: Works seamlessly with 10+ levels of nesting without workarounds (tested and verified)
  8. ✅ Type-safe composition: Full compile-time type checking with .then() method
  9. ✅ Active development: Regularly maintained with comprehensive feature set and documentation

Example: Why rust-keypaths is Better for Nested Option Chains

pl-lens approach (requires manual work):

// Manual composition - verbose and error-prone
let result = struct_instance
    .level1_field
    .as_ref()
    .and_then(|l2| l2.level2_field.as_ref())
    .and_then(|l3| l3.level3_field.as_ref())
    // ... continues for 10 levels

rust-keypaths approach (composable and type-safe):

// Clean composition - type-safe and reusable
let keypath = Level1::level1_field_fr()
    .then(Level2::level2_field_fr())
    .then(Level3::level3_field_fr())
    // ... continues for 10 levels
    .then(Level10::level10_field_fr());
    
let result = keypath.get(&instance); // Reusable, type-safe, fast

🛠 Roadmap

  • Compose across structs, options and enum cases
  • Derive macros for automatic keypath generation (Keypaths, Keypaths, Casepaths)
  • Optional chaining with failable keypaths
  • Smart pointer adapters (.for_arc(), .for_box(), .for_rc())
  • Container support for Result, Mutex, RwLock, Weak, and collections
  • Helper derive macros (ReadableKeypaths, WritableKeypaths)
  • [] Derive macros for complex multi-field enum variants

📜 License

  • Mozilla Public License 2.0