Skip to main content

rust_elm/
optics.rs

1//! Enum casepath (prism) helpers for scoped reducers and stores.
2//!
3//! State keypaths use [`Kp`] + [`RefKpTrait`](key_paths_core::RefKpTrait) from `keypath` prelude.
4//! Build casepaths with `#[derive(Cp)]` (`variant_cp()`), compose with [`.then()`](EnumKp::then),
5//! then pass to [`ScopeReducer`](crate::ScopeReducer) or [`Store::scope`](crate::Store::scope).
6
7pub use rust_key_paths::{
8    enum_err, enum_ok, enum_some, enum_variant, variant_of, EnumKp, EnumKpType, EnumValueKpType,
9    Kp,
10};
11
12/// Single-step casepath (prism): parent enum → child payload.
13pub type CasePath<'a, Parent, Child> = EnumKpType<'a, Parent, Child>;
14
15/// Extract + embed for any [`EnumKp`] (including `.then()` / `.chain()` compositions).
16pub trait Casepath<Parent, Child> {
17    fn extract(&self, parent: &Parent) -> Option<Child>;
18    fn wrap(&self, child: Child) -> Parent;
19}
20
21impl<Parent, Child, G, S, E> Casepath<Parent, Child>
22    for EnumKp<
23        Parent,
24        Child,
25        &'static Parent,
26        &'static Child,
27        &'static mut Parent,
28        &'static mut Child,
29        G,
30        S,
31        E,
32    >
33where
34    Parent: 'static,
35    Child: Clone + 'static,
36    G: for<'b> Fn(&'b Parent) -> Option<&'b Child>,
37    S: for<'b> Fn(&'b mut Parent) -> Option<&'b mut Child>,
38    E: Fn(Child) -> Parent + Clone,
39{
40    fn extract(&self, parent: &Parent) -> Option<Child> {
41        self.get_ref(parent).cloned()
42    }
43
44    fn wrap(&self, child: Child) -> Parent {
45        self.embed(child)
46    }
47}
48
49#[cfg(test)]
50#[allow(dead_code, clippy::bool_assert_comparison)]
51mod tests {
52    use super::*;
53    use key_paths_core::RefKpTrait;
54    use key_paths_derive::{Cp, Kp};
55
56    #[derive(Debug, Kp, Clone, PartialEq)]
57    struct AppState {
58        counter: Option<CounterState>,
59        label: String,
60    }
61
62    #[derive(Debug, Kp, Clone, PartialEq)]
63    struct CounterState {
64        value: i32,
65    }
66
67    #[derive(Debug, Kp, Clone, PartialEq, Eq, Cp)]
68    enum AppAction {
69        Counter(CounterAction),
70        Reset,
71    }
72
73    #[derive(Debug, Kp, Clone, Copy, PartialEq, Eq, Cp)]
74    enum CounterAction {
75        Increment,
76        Decrement,
77    }
78
79    #[derive(Clone, Debug, PartialEq, Eq, Kp, Cp)]
80    enum RootAction {
81        App(AppAction),
82    }
83
84    #[test]
85    fn ref_kp_trait_focus_round_trip() {
86        let mut state = AppState {
87            counter: Some(CounterState { value: 0 }),
88            label: "app".into(),
89        };
90        let kp = AppState::counter();
91        assert_eq!(kp.focus(&state).map(|c| c.value), Some(0));
92        if let Some(counter) = kp.focus_mut(&mut state) {
93            counter.value = 5;
94        }
95        assert_eq!(state.counter.unwrap().value, 5);
96    }
97
98    #[test]
99    fn casepath_extract_and_wrap_single_level() {
100        let kp = AppAction::counter_cp();
101        let action = AppAction::Counter(CounterAction::Increment);
102        assert_eq!(kp.extract(&action), Some(CounterAction::Increment));
103        assert_eq!(
104            kp.wrap(CounterAction::Decrement),
105            AppAction::Counter(CounterAction::Decrement)
106        );
107    }
108
109    #[test]
110    fn composed_casepath_chains_three_levels() {
111        let kp = RootAction::app_cp()
112            .then(AppAction::counter_cp());
113
114        let root = RootAction::App(AppAction::Counter(CounterAction::Increment));
115        assert_eq!(kp.extract(&root), Some(CounterAction::Increment));
116
117        let rebuilt = kp.wrap(CounterAction::Decrement);
118        assert_eq!(
119            rebuilt,
120            RootAction::App(AppAction::Counter(CounterAction::Decrement))
121        );
122    }
123
124    #[test]
125    fn then_composition_over_option_and_nested_struct() {
126        let mut state = AppState {
127            counter: Some(CounterState { value: 1 }),
128            label: "x".into(),
129        };
130        let value_kp = AppState::counter().then(CounterState::value());
131        assert_eq!(value_kp.get(&state).copied(), Some(1));
132        if let Some(v) = value_kp.get_mut(&mut state) {
133            *v = 99;
134        }
135        assert_eq!(state.counter.unwrap().value, 99);
136    }
137}