rust-key-paths 1.13.0

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 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
  • Functional chains for Arc<Mutex<T>>/Arc<RwLock<T>> - Compose keypaths through sync primitives
  • parking_lot support - Optional feature for faster locks
[dependencies]
rust-keypaths = "1.1.0"
keypaths-proc = "1.1.0"

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
  • Functional chains for Arc<Mutex<T>> and Arc<RwLock<T>> - Compose-first, apply-later pattern
  • parking_lot support - Feature-gated support for faster synchronization primitives

📦 Installation

Recommended: Static Dispatch (rust-keypaths)

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

With parking_lot Support

[dependencies]
rust-keypaths = { version = "1.1.0", features = ["parking_lot"] }
keypaths-proc = "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);

🚀 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);
    }
}

Functional Chains for Arc<Mutex> and Arc<RwLock>

Compose keypaths through synchronization primitives with a functional, compose-first approach:

use std::sync::{Arc, Mutex, RwLock};
use keypaths_proc::{Keypaths, WritableKeypaths};

#[derive(Debug, Keypaths, WritableKeypaths)]
struct Container {
    mutex_data: Arc<Mutex<DataStruct>>,
    rwlock_data: Arc<RwLock<DataStruct>>,
}

#[derive(Debug, Keypaths, WritableKeypaths)]
struct DataStruct {
    name: String,
    optional_value: Option<String>,
}

fn main() {
    let container = Container::new();
    
    // Read through Arc<Mutex<T>> - compose the chain, then apply
    Container::mutex_data_r()
        .chain_arc_mutex(DataStruct::name_r())
        .get(&container, |value| {
            println!("Name: {}", value);
        });
    
    // Write through Arc<Mutex<T>>
    Container::mutex_data_r()
        .chain_arc_mutex_writable(DataStruct::name_w())
        .get_mut(&container, |value| {
            *value = "New name".to_string();
        });
    
    // Read through Arc<RwLock<T>> (read lock)
    Container::rwlock_data_r()
        .chain_arc_rwlock(DataStruct::name_r())
        .get(&container, |value| {
            println!("Name: {}", value);
        });
    
    // Write through Arc<RwLock<T>> (write lock)
    Container::rwlock_data_r()
        .chain_arc_rwlock_writable(DataStruct::name_w())
        .get_mut(&container, |value| {
            *value = "New name".to_string();
        });
}

Available chain methods:

Method Description
chain_arc_mutex(keypath) Read through Arc<Mutex<T>>
chain_arc_mutex_optional(keypath) Optional read through Arc<Mutex<T>>
chain_arc_mutex_writable(keypath) Write through Arc<Mutex<T>>
chain_arc_mutex_writable_optional(keypath) Optional write through Arc<Mutex<T>>
chain_arc_rwlock(keypath) Read through Arc<RwLock<T>> (read lock)
chain_arc_rwlock_optional(keypath) Optional read through Arc<RwLock<T>>
chain_arc_rwlock_writable(keypath) Write through Arc<RwLock<T>> (write lock)
chain_arc_rwlock_writable_optional(keypath) Optional write through Arc<RwLock<T>>

Running the example:

cargo run --example readable_keypaths_new_containers_test

parking_lot Support

For faster synchronization with parking_lot::Mutex and parking_lot::RwLock:

[dependencies]
rust-keypaths = { version = "1.1.0", features = ["parking_lot"] }
use std::sync::Arc;
use parking_lot::{Mutex as ParkingMutex, RwLock as ParkingRwLock};

// Chain methods for parking_lot (feature-gated)
Container::parking_mutex_data_r()
    .chain_arc_parking_mutex(DataStruct::name_r())
    .get(&container, |value| {
        println!("Name: {}", value);
    });

Container::parking_rwlock_data_r()
    .chain_arc_parking_rwlock_writable(DataStruct::name_w())
    .get_mut(&container, |value| {
        *value = "New name".to_string();
    });

Key advantage: parking_lot locks never fail (no poisoning), so chain methods don't return Option for the lock operation itself.

Running the example:

cargo run --example parking_lot_chains --features parking_lot

🌟 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 Notes
Read (10 levels) 384.07 ps 848.27 ps ~464 ps absolute difference
Write (10 levels) 19.306 ns 19.338 ns 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. ✅ Functional chains for sync primitives: Compose keypaths through Arc<Mutex<T>> and Arc<RwLock<T>> with a clean, functional API
  5. ✅ parking_lot support: Feature-gated support for faster parking_lot::Mutex and parking_lot::RwLock
  6. ✅ Zero-cost abstractions: Static dispatch with minimal overhead (1.46x for reads, near-zero for writes) - benchmarked and optimized
  7. ✅ Comprehensive derive macros: Automatic generation for structs (named and tuple), enums, and all container types
  8. ✅ Swift-inspired API: Familiar API for developers coming from Swift's KeyPath system with .then() composition
  9. ✅ Deep composition: Works seamlessly with 10+ levels of nesting without workarounds (tested and verified)
  10. ✅ Type-safe composition: Full compile-time type checking with .then() method
  11. ✅ 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)
  • Functional chains for Arc<Mutex<T>> and Arc<RwLock<T>>
  • parking_lot support for faster synchronization primitives
  • Derive macros for complex multi-field enum variants

📜 License

  • Mozilla Public License 2.0