Declarative fields projection using keypath
⚓ Rust 📅 2026-03-01 👤 surdeus 👁️ 1Keypaths in Rust
A lightweight, zero-cost abstraction library for safe, composable access to nested data structures in Rust. Inspired by Functional lenses and Swift's KeyPath system, this library provides type-safe keypaths for struct fields and enum variants for superior performance.
Inspired by Lenses: Compositional Data Access And Manipulation
The library is under development GitHub - codefonsi/rust-key-paths: ReadableKeyPath and WritableKeyPath for struct and enums in Rust
KeyPaths in Rust
Safe, composable way to access and modify nested data in Rust. Inspired by KeyPath and Functional Lenses system.
[dependencies]
rust-key-paths = "2.0.6"
key-paths-derive = "2.0.6"
Basic usage
use std::sync::Arc;
use key_paths_derive::Kp;
#[derive(Debug, Kp)]
struct SomeComplexStruct {
scsf: Option<SomeOtherStruct>,
scfs2: Arc<std::sync::RwLock<SomeOtherStruct>>,
}
#[derive(Debug, Kp)]
struct SomeOtherStruct {
sosf: Option<OneMoreStruct>,
}
#[derive(Debug, Kp)]
enum SomeEnum {
A(String),
B(Box<DarkStruct>),
}
#[derive(Debug, Kp)]
struct OneMoreStruct {
omsf: Option<String>,
omse: Option<SomeEnum>,
}
#[derive(Debug, Kp)]
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(Box::new(DarkStruct {
dsf: Some(String::from("dark field")),
}))),
}),
}),
scfs2: Arc::new(std::sync::RwLock::new(SomeOtherStruct {
sosf: Some(OneMoreStruct {
omsf: Some(String::from("no value for now")),
omse: Some(SomeEnum::B(Box::new(DarkStruct {
dsf: Some(String::from("dark field")),
}))),
}),
})),
}
}
}
fn main() {
let mut instance = SomeComplexStruct::new();
SomeComplexStruct::scsf()
.then(SomeOtherStruct::sosf())
.then(OneMoreStruct::omse())
.then(SomeEnum::b())
.then(DarkStruct::dsf())
.get_mut(&mut instance).map(|x| {
*x = String::from("🖖🏿🖖🏿🖖🏿🖖🏿");
});
println!("instance = {:?}", instance.scsf.unwrap().sosf.unwrap().omse.unwrap());
// output - instance = B(DarkStruct { dsf: Some("🖖🏿🖖🏿🖖🏿🖖🏿") })
}
Composing keypaths
Chain through nested structures with then():
#[derive(Kp)]
struct Address { street: String }
#[derive(Kp)]
struct Person { address: Box<Address> }
let street_kp = Person::address().then(Address::street());
let street = street_kp.get(&person); // Option<&String>
Partial and Any keypaths
Use #[derive(Pkp, Akp)] (requires Kp) to get type-erased keypath collections:
- PKp –
partial_kps()returnsVec<PKp<Self>>; value type erased, root known - AKp –
any_kps()returnsVec<AKp>; both root and value type-erased for heterogeneous collections
Filter by value_type_id() / root_type_id() and read with get_as(). For writes, dispatch to the typed Kp (e.g. Person::name()) based on TypeId.
See examples: pkp_akp_filter_typeid, pkp_akp_read_write_convert.
Supported containers
The #[derive(Kp)] macro (from key-paths-derive) generates keypath accessors for these wrapper types:
| Container | Access | Notes |
|---|---|---|
Option<T> |
field() |
Unwraps to inner type |
Box<T> |
field() |
Derefs to inner |
Pin<T>, Pin<Box<T>> |
field(), field_inner() |
Container + inner (when T: Unpin) |
Rc<T>, Arc<T> |
field() |
Derefs; mut when unique ref |
Vec<T> |
field(), field_at(i) |
Container + index access |
HashMap<K,V>, BTreeMap<K,V> |
field_at(k) |
Key-based access |
HashSet<T>, BTreeSet<T> |
field() |
Container identity |
VecDeque<T>, LinkedList<T>, BinaryHeap<T> |
field(), field_at(i) |
Index where applicable |
Result<T,E> |
field() |
Unwraps Ok |
Cow<'_, T> |
field() |
as_ref / to_mut |
Option<Cow<'_, T>> |
field() |
Optional Cow unwrap |
std::sync::Mutex<T>, std::sync::RwLock<T> |
field() |
Container (use LockKp for lock-through) |
Arc<Mutex<T>>, Arc<RwLock<T>> |
field(), field_lock() |
Lock-through via LockKp |
tokio::sync::Mutex, tokio::sync::RwLock |
field_async() |
Async lock-through (tokio feature) |
parking_lot::Mutex, parking_lot::RwLock |
field(), field_lock() |
parking_lot feature |
1 post - 1 participant
🏷️ Rust_feed