key-paths-derive 0.7.0

Proc-macro derive to generate keypath methods
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.


✨ 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
  • ✅ Readable-only macro: #[derive(ReadableKeypaths)] for read-only access patterns
  • ✅ Writable-only macro: #[derive(WritableKeypaths)] for write-only access patterns
  • ✅ Smart keypath macro: #[derive(Keypath)] for intelligent keypath selection


🚀 Examples

See examples/ for many runnable samples. Below are a few highlights.

Readable-only keypaths for safe data access

use key_paths_derive::ReadableKeypaths;

#[derive(Debug, ReadableKeypaths)]
struct Person {
    name: String,
    age: u32,
    email: Option<String>,
    hobbies: Vec<String>,
    scores: std::collections::HashMap<String, u32>,
}

fn main() {
    let person = Person {
        name: "John Doe".to_string(),
        age: 25,
        email: Some("john@example.com".to_string()),
        hobbies: vec!["reading".to_string(), "coding".to_string()],
        scores: {
            let mut map = std::collections::HashMap::new();
            map.insert("math".to_string(), 95);
            map.insert("science".to_string(), 88);
            map
        },
    };

    // Basic readable keypaths
    println!("Name: {:?}", Person::name_r().get(&person));
    println!("Age: {:?}", Person::age_r().get(&person));

    // Failable readable keypaths
    if let Some(email) = Person::email_fr().get(&person) {
        println!("Email: {}", email);
    }

    if let Some(hobby) = Person::hobbies_fr().get(&person) {
        println!("First hobby: {}", hobby);
    }

    if let Some(score) = Person::scores_fr("math".to_string()).get(&person) {
        println!("Math score: {}", score);
    }

    // Indexed access for Vec
    if let Some(hobby) = Person::hobbies_fr_at(1).get(&person) {
        println!("Second hobby: {}", hobby);
    }
}

Writable-only keypaths for safe data mutation

use key_paths_derive::WritableKeypaths;

#[derive(Debug, WritableKeypaths)]
struct Person {
    name: String,
    age: u32,
    email: Option<String>,
    hobbies: Vec<String>,
    scores: std::collections::HashMap<String, u32>,
}

fn main() {
    let mut person = Person {
        name: "John Doe".to_string(),
        age: 25,
        email: Some("john@example.com".to_string()),
        hobbies: vec!["reading".to_string(), "coding".to_string()],
        scores: {
            let mut map = std::collections::HashMap::new();
            map.insert("math".to_string(), 95);
            map.insert("science".to_string(), 88);
            map
        },
    };

    // Basic writable keypaths
    if let Some(name_ref) = Person::name_w().get_mut(&mut person) {
        *name_ref = "John Smith".to_string();
    }

    if let Some(age_ref) = Person::age_w().get_mut(&mut person) {
        *age_ref = 26;
    }

    // Failable writable keypaths
    if let Some(email_ref) = Person::email_fw().get_mut(&mut person) {
        *email_ref = "john.smith@example.com".to_string();
    }

    if let Some(hobby_ref) = Person::hobbies_fw().get_mut(&mut person) {
        *hobby_ref = "gaming".to_string();
    }

    if let Some(score_ref) = Person::scores_fw("math".to_string()).get_mut(&mut person) {
        *score_ref = 98;
    }

    // Indexed access for Vec
    if let Some(hobby_ref) = Person::hobbies_fw_at(1).get_mut(&mut person) {
        *hobby_ref = "swimming".to_string();
    }
}

Smart keypath selection for intuitive access

use key_paths_derive::Keypath;

#[derive(Debug, Keypath)]
struct Person {
    name: String,
    age: u32,
    email: Option<String>,
    hobbies: Vec<String>,
    scores: std::collections::HashMap<String, u32>,
}

fn main() {
    let person = Person {
        name: "John Doe".to_string(),
        age: 25,
        email: Some("john@example.com".to_string()),
        hobbies: vec!["reading".to_string(), "coding".to_string()],
        scores: {
            let mut map = std::collections::HashMap::new();
            map.insert("math".to_string(), 95);
            map.insert("science".to_string(), 88);
            map
        },
    };

    // Each field gets a single method with the same name
    // The macro intelligently chooses the best keypath type:
    
    // Basic types -> readable keypath
    println!("Name: {:?}", Person::name().get(&person));
    println!("Age: {:?}", Person::age().get(&person));

    // Option<T> -> failable readable keypath to inner type
    if let Some(email) = Person::email().get(&person) {
        println!("Email: {}", email);
    }

    // Vec<T> -> failable readable keypath to first element
    if let Some(hobby) = Person::hobbies().get(&person) {
        println!("First hobby: {}", hobby);
    }

    // HashMap<K,V> -> readable keypath to container
    if let Some(scores) = Person::scores().get(&person) {
        println!("Scores: {:?}", scores);
    }
}

Widely used - Deeply nested struct

use key_paths_core::KeyPaths;
use key_paths_derive::{Casepaths, Keypaths};

#[derive(Debug, Keypaths)]
struct SomeComplexStruct {
    scsf: Option<SomeOtherStruct>,
    // scsf2: Option<SomeOtherStruct>,
}

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

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

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

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

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

fn main() {    
    let op = 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();
    let omsf = op.get_mut(&mut instance);
    *omsf.unwrap() =
        String::from("we can change the field with the other way unlocked by keypaths");
    println!("instance = {:?}", instance);

}

Iteration via keypaths

use key_paths_core::KeyPaths;

#[derive(Debug)]
struct Size {
   width: u32,
   height: u32,
}
#[derive(Debug)]
enum Color {
   Red,
   Green,
   Blue,
   Other(RGBU8),
}
#[derive(Debug)]
struct RGBU8(u8, u8, u8);

#[derive(Debug)]
struct ABox {
   name: String,
   size: Size,
   color: Color,
}
#[derive(Debug)]
struct Rectangle {
   size: Size,
   name: String,
}
fn main() {
   let mut a_box = ABox {
       name: String::from("A box"),
       size: Size {
           width: 10,
           height: 20,
       },
       color: Color::Other(
           RGBU8(10, 20, 30)
       ),
   };

   let color_kp: KeyPaths<ABox, Color> = KeyPaths::failable_writable(|x: &mut ABox| Some(&mut x.color));
   let case_path = KeyPaths::writable_enum(
       {
           |v| Color::Other(v)
       },
       |p: &Color| match p {
           Color::Other(rgb) => Some(rgb),
           _ => None,
       },
       |p: &mut Color| match p {
           Color::Other(rgb) => Some(rgb),
           _ => None,
       },

   );
   
   println!("{:?}", a_box);
   let color_rgb_kp = color_kp.compose(case_path);
   if let Some(value) = color_rgb_kp.get_mut(&mut a_box) {
       *value = RGBU8(0, 0, 0);
   }
   println!("{:?}", a_box);
}
/*
ABox { name: "A box", size: Size { width: 10, height: 20 }, color: Other(RGBU8(10, 20, 30)) }
ABox { name: "A box", size: Size { width: 10, height: 20 }, color: Other(RGBU8(0, 0, 0)) }
*/

🔗 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).

🛠 Roadmap

  • Compose across structs, options and enum cases
  • Derive macros for automatic keypath generation
  • Optional chaining with failable keypaths
  • [] Derive macros for complex multi-field enum variants

📜 License

  • Mozilla Public License 2.0