Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rust-key-paths"
version = "0.6.0"
version = "0.7.0"
edition = "2024"
authors = ["Codefonsi <[email protected]>"]
license = "MPL-2.0"
Expand All @@ -13,7 +13,7 @@ readme = "./README.md"
include = ["src/**/*", "Cargo.toml", "../../README.md", "LICENSE"]

[dependencies]
key-paths-core = { path = "key-paths-core", version = "0.6.0" }
#key-paths-core = { path = "key-paths-core", version = "0.6.0" }


[workspace]
Expand All @@ -23,7 +23,7 @@ members = [
]

[patch.crates-io]
key-paths-core = { path = "key-paths-core" }
#key-paths-core = { path = "key-paths-core" }


[features]
Expand Down
241 changes: 219 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Inspired by **Swift’s KeyPath / CasePath** system, this crate lets you work wi

```toml
[dependencies]
key_paths_core = "0.6"
rust_key_paths_ = "0.7"
```

---
Expand All @@ -30,35 +30,218 @@ key_paths_core = "0.6"
### 1. CasePaths with Enums

```rust
use key_paths_core::KeyPaths;
#[derive(Debug)]
enum Payment {
Cash { amount: u32 },
Card { number: String, cvv: String },
use rust_key_paths::{FailableWritable, ReadKeyPath, Writable, WriteKeyPath};
use rust_key_paths::Compose;

// ========== EXAMPLES ==========

// Example 1: Nested structs
#[derive(Debug, Clone)]
struct User {
name: String,
age: u32,
address: Address,
}

fn main() {
let kp = KeyPaths::writable_enum(
|v| Payment::Cash { amount: v },
|p: &Payment| match p {
Payment::Cash { amount } => Some(amount),
_ => None,
},
|p: &mut Payment| match p {
Payment::Cash { amount } => Some(amount),
#[derive(Debug, Clone)]
struct Address {
street: String,
city: String,
zip_code: String,
}

// Create keypaths for nested struct access
fn user_name_keypath() -> Writable<User, String> {
Writable::new(
|user: &User| Some(&user.name),
|user: &mut User| Some(&mut user.name),
|user: &mut User, name: String| user.name = name,
)
}

fn user_address_keypath() -> Writable<User, Address> {
Writable::new(
|user: &User| Some(&user.address),
|user: &mut User| Some(&mut user.address),
|user: &mut User, address: Address| user.address = address,
)
}

fn address_city_keypath() -> Writable<Address, String> {
Writable::new(
|addr: &Address| Some(&addr.city),
|addr: &mut Address| Some(&mut addr.city),
|addr: &mut Address, city: String| addr.city = city,
)
}

// Example 2: Enum with variants
#[derive(Debug, Clone)]
enum Contact {
Email(String),
Phone(String),
Address(Address),
Unknown,
}

#[derive(Debug, Clone)]
struct Profile {
name: String,
contact: Contact,
}

// Keypath for enum variant access (failable since variant might not match)
fn contact_email_keypath() -> FailableWritable<Contact, String> {
FailableWritable::new(|contact: &mut Contact| {
match contact {
Contact::Email(email) => Some(email),
_ => None,
}
})
}

fn profile_contact_keypath() -> Writable<Profile, Contact> {
Writable::new(
|profile: &Profile| Some(&profile.contact),
|profile: &mut Profile| Some(&mut profile.contact),
|profile: &mut Profile, contact: Contact| profile.contact = contact,
)
}

// Example 3: Complex nested structure
#[derive(Debug, Clone)]
struct Company {
name: String,
employees: Vec<Employee>,
}

#[derive(Debug, Clone)]
struct Employee {
id: u32,
profile: Profile,
salary: f64,
}

fn main() {
println!("=== Nested Struct Example ===");

let mut user = User {
name: "Alice".to_string(),
age: 30,
address: Address {
street: "123 Main St".to_string(),
city: "Springfield".to_string(),
zip_code: "12345".to_string(),
},
};

// Basic keypath usage
let name_kp = user_name_keypath();
if let Some(name) = name_kp.get(&user) {
println!("User name: {}", name);
}

name_kp.set(&mut user, "Bob".to_string());
println!("User after name change: {:?}", user);

// Composition: User -> Address -> City
let user_city_kp = user_address_keypath().then(address_city_keypath());

if let Some(city) = user_city_kp.get(&user) {
println!("User city: {}", city);
}

user_city_kp.set(&mut user, "Metropolis".to_string());
println!("User after city change: {:?}", user);

println!("\n=== Enum Example ===");

let mut profile = Profile {
name: "Charlie".to_string(),
contact: Contact::Email("[email protected]".to_string()),
};

let contact_kp = profile_contact_keypath();
let email_kp = contact_email_keypath();

// Compose profile -> contact -> email (failable)
let profile_email_kp = contact_kp.then(email_kp);

if let Some(email) = profile_email_kp.get(&profile) {
println!("Profile email: {}", email);
}

// This will work because contact is Email variant
profile_email_kp.set(&mut profile, "[email protected]".to_string());
println!("Profile after email change: {:?}", profile);

// // Change contact to Phone variant (email access will now fail)
// contact_kp.set(&mut profile, Contact::Phone("555-1234".to_string()));

if let Some(email) = profile_email_kp.get(&profile) {
println!("Profile email: {}", email);
} else {
println!("No email found (contact is now Phone variant)");
}

println!("\n=== Complex Nested Example ===");

let mut company = Company {
name: "Tech Corp".to_string(),
employees: vec![
Employee {
id: 1,
profile: Profile {
name: "Dave".to_string(),
contact: Contact::Email("[email protected]".to_string()),
},
salary: 50000.0,
},
Employee {
id: 2,
profile: Profile {
name: "Eve".to_string(),
contact: Contact::Phone("555-6789".to_string()),
},
salary: 60000.0,
},
],
};

// Create keypath for first employee's email
let first_employee_kp = Writable::new(
|company: &Company| company.employees.first(),
|company: &mut Company| company.employees.first_mut(),
|company: &mut Company, employee: Employee| {
if !company.employees.is_empty() {
company.employees[0] = employee;
}
},
);

let mut p = Payment::Cash { amount: 10 };
let employee_profile_kp = Writable::new(
|employee: &Employee| Some(&employee.profile),
|employee: &mut Employee| Some(&mut employee.profile),
|employee: &mut Employee, profile: Profile| employee.profile = profile,
);

println!("{:?}", p);
// Compose: Company -> first Employee -> Profile -> Contact -> Email
let company_first_employee_email_kp = first_employee_kp
.then(employee_profile_kp)
.then(profile_contact_keypath())
.then(contact_email_keypath());

if let Some(v) = kp.get_mut(&mut p) {
*v = 34
if let Some(email) = company_first_employee_email_kp.get(&company) {
println!("First employee email: {}", email);
}
println!("{:?}", p);

// This will work for the first employee (who has email)
company_first_employee_email_kp.set(
&mut company,
"[email protected]".to_string()
);

println!("Company after email change: {:?}", company);
}
```

Expand Down Expand Up @@ -170,7 +353,7 @@ fn main() {
println!("{garage:?}");
}
```
### 4. Mutablity
### 4. Mutability
```rust
use key_paths_core::KeyPaths;

Expand Down Expand Up @@ -253,6 +436,20 @@ ABox { name: "A box", size: Size { width: 10, height: 20 }, color: Other(RGBU8(0

---

## Support

* Struct Field Support
* Enum Variant Support
* Read / Write
* Mutability support
* Full support of Composition with keypaths including enum
* Helper macros support (WIP)
* Proc macros support (WIP)
* Feature rich, error free and light weigh 3KB only

---


## 💡 Why use KeyPaths?

* Avoids repetitive `match` / `.` chains.
Expand All @@ -267,7 +464,7 @@ ABox { name: "A box", size: Size { width: 10, height: 20 }, color: Other(RGBU8(0
* [ ] `compose` support for combining multiple key paths.
* [ ] Derive macros for automatic KeyPath generation (Upcoming).
* [ ] Nested struct & enum traversal.
* [ ] Optional chaining (`User?.profile?.name`) with failable.
* [ ] Optional chaining with failable.
---

## 📜 License
Expand Down
1 change: 1 addition & 0 deletions examples/casepath.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
1 change: 1 addition & 0 deletions examples/composeability.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
Loading