Skip to main content

rust_elm/
optics.rs

1//! State/action focusing for scoped reducers and stores.
2//!
3//! - **Keypath (lens)** — [`Kp`] / [`StateKp`] / [`StateKey`] for struct fields.
4//! - **Casepath (prism)** — [`EnumKp`] / [`CasePath`] for enum variants (extract + embed).
5//!
6//! Build casepaths with `#[derive(Cp)]` (`variant_cp()`), compose nested actions with
7//! [`.then()`](EnumKp::then) / [`.chain()`](EnumKp::chain), then pass the result to
8//! [`ScopeReducer`](crate::ScopeReducer) or [`Store::scope`](crate::Store::scope):
9//!
10//! ```ignore
11//! use key_paths_derive::{Cp, Kp};
12//!
13//! #[derive(Kp, Cp)]
14//! enum RootAction { App(AppAction) }
15//! #[derive(Kp, Cp)]
16//! enum AppAction { Panel(PanelAction) }
17//! #[derive(Kp, Cp)]
18//! enum PanelAction { Widget(WidgetAction) }
19//!
20//! let action_kp = RootAction::app_cp()
21//!     .then(AppAction::panel_cp())
22//!     .then(PanelAction::widget_cp());
23//!
24//! store.scope(state_kp, action_kp);
25//! action_kp.extract(&root_action);
26//! action_kp.wrap(WidgetAction::Submit);
27//! ```
28
29use key_paths_core::{Readable, Writable};
30
31pub use rust_key_paths::{
32    enum_err, enum_ok, enum_some, enum_variant, variant_of, EnumKp, EnumKpType, EnumValueKpType,
33    Kp, KpType,
34};
35
36/// Lens focusing `Part` within parent state `Whole`.
37///
38/// Matches the concrete [`Kp`] returned by `#[derive(Kp)]` field accessors (closure-backed
39/// `G`/`S`), not [`KpType`] (function-pointer closures).
40pub type StateKp<Whole, Part, G, Set> = Kp<
41    Whole,
42    Part,
43    &'static Whole,
44    &'static Part,
45    &'static mut Whole,
46    &'static mut Part,
47    G,
48    Set,
49>;
50
51/// Read/write navigation used by scoped reducers and stores.
52pub trait StateLens<Whole, Part> {
53    fn focus<'a>(&self, whole: &'a Whole) -> Option<&'a Part>;
54    fn focus_mut<'a>(&self, whole: &'a mut Whole) -> Option<&'a mut Part>;
55}
56
57impl<'a, R, V, G, Set> StateLens<R, V> for Kp<R, V, &'a R, &'a V, &'a mut R, &'a mut V, G, Set>
58where
59    G: for<'b> Fn(&'b R) -> Option<&'b V>,
60    Set: for<'b> Fn(&'b mut R) -> Option<&'b mut V>,
61{
62    fn focus<'b>(&self, whole: &'b R) -> Option<&'b V> {
63        self.get_ref(whole)
64    }
65
66    fn focus_mut<'b>(&self, whole: &'b mut R) -> Option<&'b mut V> {
67        self.get_mut_ref(whole)
68    }
69}
70
71/// Back-compat alias when you build keypaths with `for<'b> fn(...)` closures ([`KpType`]).
72pub type StateKey<'a, Whole, Part> = KpType<'a, Whole, Part>;
73
74/// Single-step casepath (prism): parent enum → child payload.
75pub type CasePath<'a, Parent, Child> = EnumKpType<'a, Parent, Child>;
76
77/// Back-compat alias of [`CasePath`].
78pub type ActionCase<'a, Whole, Part> = CasePath<'a, Whole, Part>;
79
80/// Back-compat alias of [`CasePath`].
81pub type ActionEnum<'a, Whole, Part> = CasePath<'a, Whole, Part>;
82
83/// Uniform extract + embed for any [`EnumKp`] (including `.then()` / `.chain()` compositions).
84pub trait Casepath<Parent, Child> {
85    fn extract(&self, parent: &Parent) -> Option<Child>;
86    fn wrap(&self, child: Child) -> Parent;
87}
88
89impl<Parent, Child, G, S, E> Casepath<Parent, Child>
90    for EnumKp<
91        Parent,
92        Child,
93        &'static Parent,
94        &'static Child,
95        &'static mut Parent,
96        &'static mut Child,
97        G,
98        S,
99        E,
100    >
101where
102    Parent: 'static,
103    Child: Clone + 'static,
104    G: for<'b> Fn(&'b Parent) -> Option<&'b Child>,
105    S: for<'b> Fn(&'b mut Parent) -> Option<&'b mut Child>,
106    E: Fn(Child) -> Parent + Clone,
107{
108    fn extract(&self, parent: &Parent) -> Option<Child> {
109        self.get_ref(parent).cloned()
110    }
111
112    fn wrap(&self, child: Child) -> Parent {
113        self.embed(child)
114    }
115}
116
117/// Read a focused `Part` from `Whole` through any keypath (lens).
118pub fn extract<'a, Whole, Part, K>(kp: &K, whole: &'a Whole) -> Option<&'a Part>
119where
120    K: Readable<&'a Whole, &'a Part>,
121{
122    kp.get(whole)
123}
124
125/// Mutably focus a `Part` within `Whole` through any keypath (lens).
126pub fn extract_mut<'a, Whole, Part, K>(kp: &K, whole: &'a mut Whole) -> Option<&'a mut Part>
127where
128    K: Writable<&'a mut Whole, &'a mut Part>,
129{
130    Writable::set(kp, whole)
131}
132
133/// Extract a child action through any [`Casepath`] (single step or composed).
134pub fn extract_action<Parent, Child, CP>(casepath: &CP, action: &Parent) -> Option<Child>
135where
136    CP: Casepath<Parent, Child>,
137{
138    casepath.extract(action)
139}
140
141/// Embed a child action through any [`Casepath`] (single step or composed).
142pub fn wrap_action<Parent, Child, CP>(casepath: &CP, child: Child) -> Parent
143where
144    CP: Casepath<Parent, Child>,
145{
146    casepath.wrap(child)
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use key_paths_derive::{Cp, Kp};
153
154    #[derive(Debug, Kp, Clone, PartialEq)]
155    struct AppState {
156        counter: Option<CounterState>,
157        label: String,
158    }
159
160    #[derive(Debug, Kp, Clone, PartialEq)]
161    struct CounterState {
162        value: i32,
163    }
164
165    #[derive(Debug, Kp, Clone, PartialEq, Eq, Cp)]
166    enum AppAction {
167        Counter(CounterAction),
168        Reset,
169    }
170
171    #[derive(Debug, Kp, Clone, Copy, PartialEq, Eq, Cp)]
172    enum CounterAction {
173        Increment,
174        Decrement,
175    }
176
177    #[derive(Clone, Debug, PartialEq, Eq, Kp, Cp)]
178    enum RootAction {
179        App(AppAction),
180    }
181
182    #[test]
183    fn state_key_get_and_get_mut_round_trip() {
184        let mut state = AppState {
185            counter: Some(CounterState { value: 0 }),
186            label: "app".into(),
187        };
188        let kp = AppState::counter();
189        assert_eq!(kp.get(&state).map(|c| c.value), Some(0));
190        if let Some(counter) = kp.get_mut(&mut state) {
191            counter.value = 5;
192        }
193        assert_eq!(state.counter.unwrap().value, 5);
194    }
195
196    #[test]
197    fn casepath_extract_and_wrap_single_level() {
198        let kp = AppAction::counter_cp();
199        let action = AppAction::Counter(CounterAction::Increment);
200        assert_eq!(extract_action(&kp, &action), Some(CounterAction::Increment));
201        assert_eq!(
202            wrap_action(&kp, CounterAction::Decrement),
203            AppAction::Counter(CounterAction::Decrement)
204        );
205    }
206
207    #[test]
208    fn composed_casepath_chains_three_levels() {
209        let kp = RootAction::app_cp()
210            .then(AppAction::counter_cp());
211
212        let root = RootAction::App(AppAction::Counter(CounterAction::Increment));
213        assert_eq!(kp.extract(&root), Some(CounterAction::Increment));
214
215        let rebuilt = kp.wrap(CounterAction::Decrement);
216        assert_eq!(
217            rebuilt,
218            RootAction::App(AppAction::Counter(CounterAction::Decrement))
219        );
220    }
221
222    #[test]
223    fn then_composition_over_option_and_nested_struct() {
224        let mut state = AppState {
225            counter: Some(CounterState { value: 1 }),
226            label: "x".into(),
227        };
228        let value_kp = AppState::counter().then(CounterState::value());
229        assert_eq!(value_kp.get(&state).copied(), Some(1));
230        if let Some(v) = value_kp.get_mut(&mut state) {
231            *v = 99;
232        }
233        assert_eq!(state.counter.unwrap().value, 99);
234    }
235}