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.
- β 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
[dependencies]
key-paths-core = "1.6.0"
key-paths-derive = "1.0.8"- One method per field:
field_name() - Smart keypath selection: Automatically chooses readable or failable readable based on field type
- No option chaining: Perfect for beginners and simple use cases
- Clean API: Just call
Struct::field_name()and you're done!
use key_paths_derive::Keypath;
#[derive(Keypath)]
struct User {
name: String, // -> User::name() returns readable keypath
email: Option<String>, // -> User::email() returns failable readable keypath
}
// Usage
let user = User { name: "Alice".into(), email: Some("alice@example.com".into()) };
let name_keypath = User::name();
let email_keypath = User::email();
let name = name_keypath.get(&user); // Some("Alice")
let email = email_keypath.get(&user); // Some("alice@example.com")- Multiple methods per field:
field_r(),field_w(),field_fr(),field_fw(),field_o(),field_fo() - Full control: Choose exactly which type of keypath you need
- Option chaining: Perfect for intermediate and advanced developers
- Comprehensive: Supports all container types and access patterns
use key_paths_derive::Keypaths;
#[derive(Keypaths)]
struct User {
name: String,
email: Option<String>,
}
// Usage - you choose the exact method
let user = User { name: "Alice".into(), email: Some("alice@example.com".into()) };
let name_keypath = User::name_r();
let email_keypath = User::email_fr();
let name = name_keypath.get(&user); // Some("Alice") - readable
let email = email_keypath.get(&user); // Some("alice@example.com") - failable readableuse key_paths_derive::{Casepaths, Keypaths};
#[derive(Debug, Keypaths)]
struct SomeComplexStruct {
scsf: Option<SomeOtherStruct>,
}
#[derive(Debug, Keypaths)]
struct SomeOtherStruct {
sosf: Option<OneMoreStruct>,
}
#[derive(Debug, Keypaths)]
struct OneMoreStruct {
omsf: Option<String>,
omse: Option<SomeEnum>,
}
#[derive(Debug, Casepaths)]
enum SomeEnum {
A(String),
B(DarkStruct),
}
#[derive(Debug, Keypaths)]
struct DarkStruct {
dsf: Option<String>,
}
impl SomeComplexStruct {
fn new() -> Self {
Self {
scsf: Some(SomeOtherStruct {
sosf: Some(OneMoreStruct {
omsf: Some(String::from("no value for now")),
omse: Some(SomeEnum::B(DarkStruct {
dsf: Some(String::from("dark field")),
})),
}),
}),
}
}
}
fn main() {
let dsf_kp = SomeComplexStruct::scsf_fw()
.then(SomeOtherStruct::sosf_fw())
.then(OneMoreStruct::omse_fw())
.then(SomeEnum::b_case_w())
.then(DarkStruct::dsf_fw());
let mut instance = SomeComplexStruct::new();
if let Some(omsf) = dsf_kp.get_mut(&mut instance) {
*omsf = String::from("This is changed ππΏ");
println!("instance = {:?}", instance);
}
}Recommendation: Start with #[derive(Keypath)] for simplicity, upgrade to #[derive(Keypaths)] when you need more control!
| Feature | #[derive(Keypath)] |
#[derive(Keypaths)] |
|---|---|---|
| API Complexity | Simple - one method per field | Advanced - multiple methods per field |
| Learning Curve | Beginner-friendly | Requires understanding of keypath types |
| Container Support | Basic containers only | Full container support including Result, Mutex, RwLock, Wea****k |
| Option Chaining | No - smart selection only | Yes - full control over failable vs non-failable |
| Writable Access | Limited | Full writable support |
| Use Case | Simple field access, beginners | Complex compositions, advanced users |
When to use Keypath:
- You're new to keypaths
- You want simple, clean field access
- You don't need complex option chaining
- You're working with basic types
When to use Keypaths:
- You need full control over keypath types
- You're composing complex nested structures
- You need writable access to fields
- You're working with advanced container types
See examples/ for many runnable samples. Below are a few highlights.
use key_paths_derive::Keypath;
#[derive(Keypath)]
struct User {
name: String,
age: u32,
email: Option<String>,
}
fn main() {
let user = User {
name: "Alice".to_string(),
age: 30,
email: Some("alice@example.com".to_string()),
};
// Access fields using keypaths
let name_keypath = User::name();
let age_keypath = User::age();
let email_keypath = User::email();
let name = name_keypath.get(&user); // Some("Alice")
let age = age_keypath.get(&user); // Some(30)
let email = email_keypath.get(&user); // Some("alice@example.com")
println!("Name: {:?}", name);
println!("Age: {:?}", age);
println!("Email: {:?}", email);
}KeyPaths now support smart pointers, containers, and references via adapter methods:
Use .for_arc(), .for_box(), or .for_rc() to adapt keypaths for wrapped types:
use key_paths_derive::Keypaths;
use std::sync::Arc;
#[derive(Keypaths)]
struct Product {
name: String,
price: f64,
}
let products: Vec<Arc<Product>> = vec![
Arc::new(Product { name: "Laptop".into(), price: 999.99 }),
];
// Adapt keypath to work with Arc<Product>
let price_path = Product::price().for_arc();
let affordable: Vec<&Arc<Product>> = products
.iter()
.filter(|p| price_path.get(p).map_or(false, |&price| price < 100.0))
.collect();Use .get_ref() and .get_mut_ref() for collections of references:
use key_paths_derive::Keypaths;
#[derive(Keypaths)]
struct Product {
name: String,
price: f64,
}
let products: Vec<&Product> = hashmap.values().collect();
let price_path = Product::price();
for product_ref in &products {
if let Some(&price) = price_path.get_ref(product_ref) {
println!("Price: ${}", price);
}
}Supported Adapters:
.for_arc()- Works withArc<T>(read-only).for_box()- Works withBox<T>(read & write).for_rc()- Works withRc<T>(read-only).get_ref()- Works with&Treferences.get_mut_ref()- Works with&mut Treferences
Examples:
examples/container_adapters.rs- Smart pointer usageexamples/reference_keypaths.rs- Reference collectionskey-paths-core/examples/container_adapter_test.rs- Test suite
Documentation: See CONTAINER_ADAPTERS.md and REFERENCE_SUPPORT.md
The rust-key-paths library is being used by several exciting crates in the Rust ecosystem:
- π rust-queries-builder - Type-safe, SQL-like queries for in-memory collections
- π rust-overture - Functional programming utilities and abstractions
- π rust-prelude-plus - Enhanced prelude with additional utilities and traits
- π type-safe property paths
- π Swift KeyPath documentation
- π Elm Architecture & Functional Lenses
- π Rust Macros Book
- π Category Theory in FP (for intuition)
- 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).
- 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
- Mozilla Public License 2.0