Skip to main content

Crate lenz

Crate lenz 

Source
Expand description

§Lenz

License Crates.io Docs CI Discord

Lenz turns a path to a deeply nested field into a reusable handle you pass around and use later to read or write it. Paths live in the type system, checked at compile time, zero-cost at runtime.

§Example

use lenz::{Cursor, Lenz, Tagged};

// A tag type. `lenz` only carries it; a downstream crate attaches the
// meaning (say, "how to write this field to a backend").
pub struct SetText;

#[derive(Lenz)]
pub struct Label {
    #[lenz(tag = SetText)]
    pub text: String,
    pub size: u32,
}

#[derive(Lenz)]
pub struct Button {
    pub label: Label,
    pub icon: Option<Label>,
    #[lenz(ignore)]
    pub pressed: bool,
}

let mut button = Button {
    label: Label { text: "Save".into(), size: 14 },
    icon: None,
    pressed: false,
};

// Walk from the root to a nested field. Each `.field()` adds a hop;
// the `Option` on `icon` is just another link.
let text = Button::cursor().label().text();

// `.accessor()` ends the walk: two `fn` pointers, `Copy`, no alloc.
let acc = text.accessor();
assert_eq!(acc.get(&button).map(String::as_str), Some("Save"));
*acc.get_mut(&mut button).unwrap() = "Saved".into();

// A walk through an absent `Option` returns `None`, never panics.
let icon_text = Button::cursor().icon().text().accessor();
assert_eq!(icon_text.get(&button), None);

// `.hops()` lists the links; `.key()` names the whole walk.
assert_eq!(Button::cursor().label().text().hops().len(), 2);

// `text` is tagged, so its path carries `SetText` as its `Tag`.
fn writes_text<P: Tagged<Tag = SetText>>(_: Cursor<P>) {}
writes_text(Button::cursor().label().text());

§How it works

#[derive(Lenz)] generates, for each field: a zero-sized FieldPath marker and a {Struct}Cursor method that walks to it. Struct::cursor() starts a walk at the root; each .field() call chains another link, and the B::Source == A::Target bound rejects a mismatched join at compile time.

End a walk one of these ways:

  • .accessor(): collapses the path into an Accessor<S, T> - two fn pointers, Copy, no allocation. get / get_mut return an Option, None if any Option along the way was absent, so a caller never has to know which links were optional.
  • .key(): one FieldId naming the whole walk, distinct from any single hop’s id, so top.text and bottom.text key separately.
  • .hops(): a FieldId per link, the route a patch follows.

A FieldId wraps the path type’s own TypeId, so an id can only come from a real path.

§Attributes

  • #[lenz(ignore)]: the field gets no marker and no cursor method, so nothing can name a path to it.

  • #[lenz(tag = <path>)]: the field’s marker also implements Tagged, with <path> as the associated Tag type. lenz never inspects it, and a Chain carries the tag of its last hop, so a composed walk exposes the tag of the field it ends on.

  • #[lenz(crate = <path>)]: on the struct, points the generated code at a re-exported lenz, for a struct emitted by another crate’s macro:

    #[derive(Lenz)]
    #[lenz(crate = ::my_framework::lenz)]
    pub struct Generated { /* ... */ }

§no_std

lenz is #![no_std]; it uses alloc for the Vec that Cursor::hops returns.

§License

lenz is dual-licensed under either:

at your option.

Structs§

Accessor
What a finished path collapses to: two flat function pointers, Copy, no allocation.
Chain
A, then B. The B::Source == A::Target bound rejects a mismatched join at compile time, and the walk short circuits if either link is absent.
Cursor
Where the walk is standing. Carries the path so far as P, nothing at runtime.
FieldId
What a path is called once the types are gone.
Identity
The empty path, rooting every walk.

Traits§

FieldPath
A path from Source to Target, composed at the type level.
Tagged
A path whose terminal hop carries a Tag, an opaque type set on the field’s marker by #[lenz(tag = ...)]. A Chain takes its last hop’s tag, so a walk exposes the tag of the field it ends on.

Derive Macros§

Lenz
Generates the field paths for a struct: a zero-sized FieldPath marker per field, and a Cursor method that walks to it. Call accessor() to end the walk.