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, RwLock};
14
15use crate::runtime::state_access::{StateRead, StateWrite};
16
17/// Read/write access to store state without cloning `S`.
18#[derive(Clone)]
19pub struct StateBinding<S: 'static> {
20    state: Arc<Mutex<S>>,
21}
22
23impl<S: 'static> StateBinding<S> {
24    pub(crate) fn new(state: Arc<Mutex<S>>) -> Self {
25        Self { state }
26    }
27
28    /// Borrow the root state under the lock.
29    pub fn with<R>(&self, f: impl FnOnce(&S) -> R) -> R {
30        f(&*self.state.lock())
31    }
32
33    /// Mutably borrow the root state under the lock.
34    pub fn with_mut<R>(&self, f: impl FnOnce(&mut S) -> R) -> R {
35        f(&mut *self.state.lock())
36    }
37
38    /// Project a property via keypath — reads/writes go to the original binding's state.
39    pub fn project<V, SK>(&self, kp: SK) -> ProjectedBinding<S, V, SK>
40    where
41        SK: RefKpTrait<S, V>,
42    {
43        ProjectedBinding {
44            state: Arc::clone(&self.state),
45            kp,
46            _marker: PhantomData,
47        }
48    }
49}
50
51/// A field projected from a [`StateBinding`] through a keypath.
52pub struct ProjectedBinding<Root: 'static, Focus: 'static, SK> {
53    state: Arc<Mutex<Root>>,
54    kp: SK,
55    _marker: PhantomData<Focus>,
56}
57
58impl<Root, Focus, SK> Clone for ProjectedBinding<Root, Focus, SK>
59where
60    Root: 'static,
61    Focus: 'static,
62    SK: RefKpTrait<Root, Focus> + Clone,
63{
64    fn clone(&self) -> Self {
65        Self {
66            state: Arc::clone(&self.state),
67            kp: self.kp.clone(),
68            _marker: PhantomData,
69        }
70    }
71}
72
73impl<Root, Focus, SK> ProjectedBinding<Root, Focus, SK>
74where
75    Root: 'static,
76    Focus: 'static,
77    SK: RefKpTrait<Root, Focus>,
78{
79    /// Borrow the focused value under the root lock, or [`None`] if the keypath misses.
80    pub fn with<R>(&self, f: impl FnOnce(&Focus) -> R) -> Option<R> {
81        let guard = self.state.lock();
82        let focused = self.kp.focus(&*guard)?;
83        Some(f(focused))
84    }
85
86    /// Mutably borrow the focused value under the root lock, or [`None`] if the keypath misses.
87    pub fn with_mut<R>(&self, f: impl FnOnce(&mut Focus) -> R) -> Option<R> {
88        let mut guard = self.state.lock();
89        let focused = self.kp.focus_mut(&mut *guard)?;
90        Some(f(focused))
91    }
92
93    /// Chain another keypath segment (consumes `self`; parent keypath must be [`Clone`]).
94    pub fn project<V, SubSK>(self, sub_kp: SubSK) -> ComposedBinding<Root, Focus, V, SK, SubSK>
95    where
96        SubSK: RefKpTrait<Focus, V>,
97        SK: Clone,
98    {
99        ComposedBinding {
100            state: self.state,
101            parent_kp: self.kp,
102            sub_kp,
103            _marker: PhantomData,
104        }
105    }
106}
107
108/// Two-segment keypath projection from root state.
109pub struct ComposedBinding<Root: 'static, Mid: 'static, Focus: 'static, ParentKP, SubKP> {
110    state: Arc<Mutex<Root>>,
111    parent_kp: ParentKP,
112    sub_kp: SubKP,
113    _marker: PhantomData<(Mid, Focus)>,
114}
115
116impl<Root, Mid, Focus, ParentKP, SubKP> ComposedBinding<Root, Mid, Focus, ParentKP, SubKP>
117where
118    Root: 'static,
119    Mid: 'static,
120    Focus: 'static,
121    ParentKP: RefKpTrait<Root, Mid>,
122    SubKP: RefKpTrait<Mid, Focus>,
123{
124    /// Borrow through parent then child keypath; [`None`] if either segment misses.
125    pub fn with<R>(&self, f: impl FnOnce(&Focus) -> R) -> Option<R> {
126        let guard = self.state.lock();
127        let mid = self.parent_kp.focus(&*guard)?;
128        let focused = self.sub_kp.focus(mid)?;
129        Some(f(focused))
130    }
131
132    /// Mutably borrow through parent then child keypath; [`None`] if either segment misses.
133    pub fn with_mut<R>(&self, f: impl FnOnce(&mut Focus) -> R) -> Option<R> {
134        let mut guard = self.state.lock();
135        let mid = self.parent_kp.focus_mut(&mut *guard)?;
136        let focused = self.sub_kp.focus_mut(mid)?;
137        Some(f(focused))
138    }
139}
140
141// ── RwLock-backed bindings (concurrent readers) ─────────────────────────────
142
143/// Read-only binding over [`RwLock`] state — many concurrent readers.
144#[derive(Clone)]
145pub struct ReadStateBinding<S: 'static> {
146    state: Arc<RwLock<S>>,
147}
148
149impl<S: 'static> ReadStateBinding<S> {
150    pub(crate) fn new(state: Arc<RwLock<S>>) -> Self {
151        Self { state }
152    }
153
154    pub fn with_read<R>(&self, f: impl FnOnce(&S) -> R) -> R {
155        self.state.with_read(f)
156    }
157
158    pub fn project<V, SK>(&self, kp: SK) -> ReadProjectedBinding<S, V, SK>
159    where
160        SK: RefKpTrait<S, V>,
161    {
162        ReadProjectedBinding {
163            state: Arc::clone(&self.state),
164            kp,
165            _marker: PhantomData,
166        }
167    }
168}
169
170/// Read/write binding over [`RwLock`] state — writes take exclusive lock.
171#[derive(Clone)]
172pub struct RwStateBinding<S: 'static> {
173    state: Arc<RwLock<S>>,
174}
175
176impl<S: 'static> RwStateBinding<S> {
177    pub(crate) fn new(state: Arc<RwLock<S>>) -> Self {
178        Self { state }
179    }
180
181    pub fn with_read<R>(&self, f: impl FnOnce(&S) -> R) -> R {
182        self.state.with_read(f)
183    }
184
185    pub fn with_write<R>(&self, f: impl FnOnce(&mut S) -> R) -> R {
186        self.state.with_write(f)
187    }
188
189    pub fn read_store(&self) -> ReadStateBinding<S> {
190        ReadStateBinding::new(Arc::clone(&self.state))
191    }
192
193    pub fn project<V, SK>(&self, kp: SK) -> RwProjectedBinding<S, V, SK>
194    where
195        SK: RefKpTrait<S, V>,
196    {
197        RwProjectedBinding {
198            state: Arc::clone(&self.state),
199            kp,
200            _marker: PhantomData,
201        }
202    }
203}
204
205/// Read-only projected field from [`ReadStateBinding`].
206pub struct ReadProjectedBinding<Root: 'static, Focus: 'static, SK> {
207    state: Arc<RwLock<Root>>,
208    kp: SK,
209    _marker: PhantomData<Focus>,
210}
211
212impl<Root, Focus, SK> Clone for ReadProjectedBinding<Root, Focus, SK>
213where
214    Root: 'static,
215    Focus: 'static,
216    SK: RefKpTrait<Root, Focus> + Clone,
217{
218    fn clone(&self) -> Self {
219        Self {
220            state: Arc::clone(&self.state),
221            kp: self.kp.clone(),
222            _marker: PhantomData,
223        }
224    }
225}
226
227impl<Root, Focus, SK> ReadProjectedBinding<Root, Focus, SK>
228where
229    Root: 'static,
230    Focus: 'static,
231    SK: RefKpTrait<Root, Focus>,
232{
233    pub fn with_read<R>(&self, f: impl FnOnce(&Focus) -> R) -> Option<R> {
234        let guard = self.state.read();
235        let focused = self.kp.focus(&*guard)?;
236        Some(f(focused))
237    }
238}
239
240/// Projected field from [`RwStateBinding`].
241pub struct RwProjectedBinding<Root: 'static, Focus: 'static, SK> {
242    state: Arc<RwLock<Root>>,
243    kp: SK,
244    _marker: PhantomData<Focus>,
245}
246
247impl<Root, Focus, SK> Clone for RwProjectedBinding<Root, Focus, SK>
248where
249    Root: 'static,
250    Focus: 'static,
251    SK: RefKpTrait<Root, Focus> + Clone,
252{
253    fn clone(&self) -> Self {
254        Self {
255            state: Arc::clone(&self.state),
256            kp: self.kp.clone(),
257            _marker: PhantomData,
258        }
259    }
260}
261
262impl<Root, Focus, SK> RwProjectedBinding<Root, Focus, SK>
263where
264    Root: 'static,
265    Focus: 'static,
266    SK: RefKpTrait<Root, Focus>,
267{
268    pub fn with_read<R>(&self, f: impl FnOnce(&Focus) -> R) -> Option<R> {
269        let guard = self.state.read();
270        let focused = self.kp.focus(&*guard)?;
271        Some(f(focused))
272    }
273
274    pub fn with_write<R>(&self, f: impl FnOnce(&mut Focus) -> R) -> Option<R> {
275        let mut guard = self.state.write();
276        let focused = self.kp.focus_mut(&mut *guard)?;
277        Some(f(focused))
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use key_paths_derive::{FieldDiff, Kp};
285    use rust_key_paths::Kp as KpPath;
286
287    #[derive(Debug, Kp, Clone, Hash, FieldDiff, PartialEq)]
288    struct Episode {
289        current_position: i32,
290        is_favorite: bool,
291    }
292
293    fn position_kp() -> KpPath<
294        Episode,
295        i32,
296        &'static Episode,
297        &'static i32,
298        &'static mut Episode,
299        &'static mut i32,
300        for<'b> fn(&'b Episode) -> Option<&'b i32>,
301        for<'b> fn(&'b mut Episode) -> Option<&'b mut i32>,
302    > {
303        fn get(e: &Episode) -> Option<&i32> {
304            Some(&e.current_position)
305        }
306        fn get_mut(e: &mut Episode) -> Option<&mut i32> {
307            Some(&mut e.current_position)
308        }
309        KpPath::new(get, get_mut)
310    }
311
312    fn favorite_kp() -> KpPath<
313        Episode,
314        bool,
315        &'static Episode,
316        &'static bool,
317        &'static mut Episode,
318        &'static mut bool,
319        for<'b> fn(&'b Episode) -> Option<&'b bool>,
320        for<'b> fn(&'b mut Episode) -> Option<&'b mut bool>,
321    > {
322        fn get(e: &Episode) -> Option<&bool> {
323            Some(&e.is_favorite)
324        }
325        fn get_mut(e: &mut Episode) -> Option<&mut bool> {
326            Some(&mut e.is_favorite)
327        }
328        KpPath::new(get, get_mut)
329    }
330
331    #[test]
332    fn binding_projects_fields_without_cloning_root() {
333        let state = Arc::new(Mutex::new(Episode {
334            current_position: 1,
335            is_favorite: false,
336        }));
337        let binding = StateBinding::new(state);
338
339        let position = binding.project(position_kp());
340        assert_eq!(position.with(|p| *p), Some(1));
341
342        position.with_mut(|p| *p = 2);
343        assert_eq!(binding.with(|e| e.current_position), 2);
344
345        let favorite = binding.project(favorite_kp());
346        favorite.with_mut(|f| *f = true);
347        assert!(binding.with(|e| e.is_favorite));
348    }
349
350    #[test]
351    fn projected_binding_returns_none_when_keypath_misses() {
352        #[derive(Kp, Clone, Hash, FieldDiff, PartialEq, Debug)]
353        struct Root {
354            child: Option<Episode>,
355        }
356
357        fn child_kp() -> KpPath<
358            Root,
359            Episode,
360            &'static Root,
361            &'static Episode,
362            &'static mut Root,
363            &'static mut Episode,
364            for<'b> fn(&'b Root) -> Option<&'b Episode>,
365            for<'b> fn(&'b mut Root) -> Option<&'b mut Episode>,
366        > {
367            fn get(r: &Root) -> Option<&Episode> {
368                r.child.as_ref()
369            }
370            fn get_mut(r: &mut Root) -> Option<&mut Episode> {
371                r.child.as_mut()
372            }
373            KpPath::new(get, get_mut)
374        }
375
376        let binding = StateBinding::new(Arc::new(Mutex::new(Root { child: None })));
377        let child = binding.project(child_kp());
378        assert!(child.with(|_| ()).is_none());
379        assert!(child.with_mut(|_| ()).is_none());
380    }
381}