Skip to main content

rust_elm/runtime/
binding.rs

1//! Lock-backed bindings with keypath projection (SwiftUI `Binding` dynamic-member style).
2//!
3//! Reads and writes go through the store's shared mutex — no full-state clone.
4//! Project a field with [`StateBinding::project`] / [`ProjectedBinding::project`].
5//!
6//! When a keypath cannot focus (e.g. `None` in an `Option` field), [`ProjectedBinding::with`]
7//! and [`ProjectedBinding::with_mut`] return [`None`] instead of panicking.
8
9use std::marker::PhantomData;
10use std::sync::Arc;
11
12use key_paths_core::RefKpTrait;
13use parking_lot::Mutex;
14
15/// Read/write access to store state without cloning `S`.
16#[derive(Clone)]
17pub struct StateBinding<S: 'static> {
18    state: Arc<Mutex<S>>,
19}
20
21impl<S: 'static> StateBinding<S> {
22    pub(crate) fn new(state: Arc<Mutex<S>>) -> Self {
23        Self { state }
24    }
25
26    /// Borrow the root state under the lock.
27    pub fn with<R>(&self, f: impl FnOnce(&S) -> R) -> R {
28        f(&*self.state.lock())
29    }
30
31    /// Mutably borrow the root state under the lock.
32    pub fn with_mut<R>(&self, f: impl FnOnce(&mut S) -> R) -> R {
33        f(&mut *self.state.lock())
34    }
35
36    /// Project a property via keypath — reads/writes go to the original binding's state.
37    pub fn project<V, SK>(&self, kp: SK) -> ProjectedBinding<S, V, SK>
38    where
39        SK: RefKpTrait<S, V>,
40    {
41        ProjectedBinding {
42            state: Arc::clone(&self.state),
43            kp,
44            _marker: PhantomData,
45        }
46    }
47}
48
49/// A field projected from a [`StateBinding`] through a keypath.
50pub struct ProjectedBinding<Root: 'static, Focus: 'static, SK> {
51    state: Arc<Mutex<Root>>,
52    kp: SK,
53    _marker: PhantomData<Focus>,
54}
55
56impl<Root, Focus, SK> Clone for ProjectedBinding<Root, Focus, SK>
57where
58    Root: 'static,
59    Focus: 'static,
60    SK: RefKpTrait<Root, Focus> + Clone,
61{
62    fn clone(&self) -> Self {
63        Self {
64            state: Arc::clone(&self.state),
65            kp: self.kp.clone(),
66            _marker: PhantomData,
67        }
68    }
69}
70
71impl<Root, Focus, SK> ProjectedBinding<Root, Focus, SK>
72where
73    Root: 'static,
74    Focus: 'static,
75    SK: RefKpTrait<Root, Focus>,
76{
77    /// Borrow the focused value under the root lock, or [`None`] if the keypath misses.
78    pub fn with<R>(&self, f: impl FnOnce(&Focus) -> R) -> Option<R> {
79        let guard = self.state.lock();
80        let focused = self.kp.focus(&*guard)?;
81        Some(f(focused))
82    }
83
84    /// Mutably borrow the focused value under the root lock, or [`None`] if the keypath misses.
85    pub fn with_mut<R>(&self, f: impl FnOnce(&mut Focus) -> R) -> Option<R> {
86        let mut guard = self.state.lock();
87        let focused = self.kp.focus_mut(&mut *guard)?;
88        Some(f(focused))
89    }
90
91    /// Chain another keypath segment (consumes `self`; parent keypath must be [`Clone`]).
92    pub fn project<V, SubSK>(self, sub_kp: SubSK) -> ComposedBinding<Root, Focus, V, SK, SubSK>
93    where
94        SubSK: RefKpTrait<Focus, V>,
95        SK: Clone,
96    {
97        ComposedBinding {
98            state: self.state,
99            parent_kp: self.kp,
100            sub_kp,
101            _marker: PhantomData,
102        }
103    }
104}
105
106/// Two-segment keypath projection from root state.
107pub struct ComposedBinding<Root: 'static, Mid: 'static, Focus: 'static, ParentKP, SubKP> {
108    state: Arc<Mutex<Root>>,
109    parent_kp: ParentKP,
110    sub_kp: SubKP,
111    _marker: PhantomData<(Mid, Focus)>,
112}
113
114impl<Root, Mid, Focus, ParentKP, SubKP> ComposedBinding<Root, Mid, Focus, ParentKP, SubKP>
115where
116    Root: 'static,
117    Mid: 'static,
118    Focus: 'static,
119    ParentKP: RefKpTrait<Root, Mid>,
120    SubKP: RefKpTrait<Mid, Focus>,
121{
122    /// Borrow through parent then child keypath; [`None`] if either segment misses.
123    pub fn with<R>(&self, f: impl FnOnce(&Focus) -> R) -> Option<R> {
124        let guard = self.state.lock();
125        let mid = self.parent_kp.focus(&*guard)?;
126        let focused = self.sub_kp.focus(mid)?;
127        Some(f(focused))
128    }
129
130    /// Mutably borrow through parent then child keypath; [`None`] if either segment misses.
131    pub fn with_mut<R>(&self, f: impl FnOnce(&mut Focus) -> R) -> Option<R> {
132        let mut guard = self.state.lock();
133        let mid = self.parent_kp.focus_mut(&mut *guard)?;
134        let focused = self.sub_kp.focus_mut(mid)?;
135        Some(f(focused))
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use key_paths_derive::{FieldDiff, Kp};
143    use rust_key_paths::Kp as KpPath;
144
145    #[derive(Debug, Kp, Clone, Hash, FieldDiff, PartialEq)]
146    struct Episode {
147        current_position: i32,
148        is_favorite: bool,
149    }
150
151    fn position_kp() -> KpPath<
152        Episode,
153        i32,
154        &'static Episode,
155        &'static i32,
156        &'static mut Episode,
157        &'static mut i32,
158        for<'b> fn(&'b Episode) -> Option<&'b i32>,
159        for<'b> fn(&'b mut Episode) -> Option<&'b mut i32>,
160    > {
161        fn get(e: &Episode) -> Option<&i32> {
162            Some(&e.current_position)
163        }
164        fn get_mut(e: &mut Episode) -> Option<&mut i32> {
165            Some(&mut e.current_position)
166        }
167        KpPath::new(get, get_mut)
168    }
169
170    fn favorite_kp() -> KpPath<
171        Episode,
172        bool,
173        &'static Episode,
174        &'static bool,
175        &'static mut Episode,
176        &'static mut bool,
177        for<'b> fn(&'b Episode) -> Option<&'b bool>,
178        for<'b> fn(&'b mut Episode) -> Option<&'b mut bool>,
179    > {
180        fn get(e: &Episode) -> Option<&bool> {
181            Some(&e.is_favorite)
182        }
183        fn get_mut(e: &mut Episode) -> Option<&mut bool> {
184            Some(&mut e.is_favorite)
185        }
186        KpPath::new(get, get_mut)
187    }
188
189    #[test]
190    fn binding_projects_fields_without_cloning_root() {
191        let state = Arc::new(Mutex::new(Episode {
192            current_position: 1,
193            is_favorite: false,
194        }));
195        let binding = StateBinding::new(state);
196
197        let position = binding.project(position_kp());
198        assert_eq!(position.with(|p| *p), Some(1));
199
200        position.with_mut(|p| *p = 2);
201        assert_eq!(binding.with(|e| e.current_position), 2);
202
203        let favorite = binding.project(favorite_kp());
204        favorite.with_mut(|f| *f = true);
205        assert!(binding.with(|e| e.is_favorite));
206    }
207
208    #[test]
209    fn projected_binding_returns_none_when_keypath_misses() {
210        #[derive(Kp, Clone, Hash, FieldDiff, PartialEq, Debug)]
211        struct Root {
212            child: Option<Episode>,
213        }
214
215        fn child_kp() -> KpPath<
216            Root,
217            Episode,
218            &'static Root,
219            &'static Episode,
220            &'static mut Root,
221            &'static mut Episode,
222            for<'b> fn(&'b Root) -> Option<&'b Episode>,
223            for<'b> fn(&'b mut Root) -> Option<&'b mut Episode>,
224        > {
225            fn get(r: &Root) -> Option<&Episode> {
226                r.child.as_ref()
227            }
228            fn get_mut(r: &mut Root) -> Option<&mut Episode> {
229                r.child.as_mut()
230            }
231            KpPath::new(get, get_mut)
232        }
233
234        let binding = StateBinding::new(Arc::new(Mutex::new(Root { child: None })));
235        let child = binding.project(child_kp());
236        assert!(child.with(|_| ()).is_none());
237        assert!(child.with_mut(|_| ()).is_none());
238    }
239}