presentar_core/
state.rs

1//! State management for Presentar applications.
2//!
3//! This module implements the Elm Architecture pattern for predictable state
4//! management: `State + Message → (State, Command)`.
5//!
6//! # Examples
7//!
8//! ```
9//! use presentar_core::{State, Command};
10//! use serde::{Deserialize, Serialize};
11//!
12//! // Define your application state
13//! #[derive(Clone, Default, Serialize, Deserialize)]
14//! struct AppState {
15//!     count: i32,
16//! }
17//!
18//! // Define messages that modify state
19//! enum AppMessage {
20//!     Increment,
21//!     Decrement,
22//!     Reset,
23//! }
24//!
25//! impl State for AppState {
26//!     type Message = AppMessage;
27//!
28//!     fn update(&mut self, msg: Self::Message) -> Command<Self::Message> {
29//!         match msg {
30//!             AppMessage::Increment => self.count += 1,
31//!             AppMessage::Decrement => self.count -= 1,
32//!             AppMessage::Reset => self.count = 0,
33//!         }
34//!         Command::None
35//!     }
36//! }
37//!
38//! let mut state = AppState::default();
39//! state.update(AppMessage::Increment);
40//! assert_eq!(state.count, 1);
41//! ```
42
43use serde::{Deserialize, Serialize};
44use std::future::Future;
45use std::pin::Pin;
46
47/// Application state trait.
48///
49/// Implements the Elm Architecture: State + Message → (State, Command)
50pub trait State: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync {
51    /// Message type for state updates
52    type Message: Send;
53
54    /// Update state in response to a message.
55    ///
56    /// Returns a command for side effects (async operations, navigation, etc.)
57    fn update(&mut self, msg: Self::Message) -> Command<Self::Message>;
58}
59
60/// Commands for side effects.
61///
62/// Commands represent effects that should happen after a state update:
63/// - Async tasks (data fetching, file operations)
64/// - Navigation
65/// - State persistence
66#[derive(Default)]
67pub enum Command<M> {
68    /// No command
69    #[default]
70    None,
71    /// Execute multiple commands
72    Batch(Vec<Command<M>>),
73    /// Execute an async task
74    Task(Pin<Box<dyn Future<Output = M> + Send>>),
75    /// Navigate to a route
76    Navigate {
77        /// Route path
78        route: String,
79    },
80    /// Save state to storage
81    SaveState {
82        /// Storage key
83        key: String,
84    },
85    /// Load state from storage
86    LoadState {
87        /// Storage key
88        key: String,
89        /// Callback with loaded state
90        on_load: fn(Option<Vec<u8>>) -> M,
91    },
92}
93
94impl<M> Command<M> {
95    /// Create a task command from an async block.
96    pub fn task<F>(future: F) -> Self
97    where
98        F: Future<Output = M> + Send + 'static,
99    {
100        Self::Task(Box::pin(future))
101    }
102
103    /// Create a batch of commands.
104    pub fn batch(commands: impl IntoIterator<Item = Self>) -> Self {
105        Self::Batch(commands.into_iter().collect())
106    }
107
108    /// Check if this is the none command.
109    #[must_use]
110    pub const fn is_none(&self) -> bool {
111        matches!(self, Self::None)
112    }
113
114    /// Map the message type using a function.
115    pub fn map<N, F>(self, f: F) -> Command<N>
116    where
117        F: Fn(M) -> N + Send + Sync + 'static,
118        M: Send + 'static,
119        N: Send + 'static,
120    {
121        let f: std::sync::Arc<dyn Fn(M) -> N + Send + Sync> = std::sync::Arc::new(f);
122        self.map_inner(&f)
123    }
124
125    fn map_inner<N>(self, f: &std::sync::Arc<dyn Fn(M) -> N + Send + Sync>) -> Command<N>
126    where
127        M: Send + 'static,
128        N: Send + 'static,
129    {
130        match self {
131            Self::None => Command::None,
132            Self::Batch(cmds) => Command::Batch(cmds.into_iter().map(|c| c.map_inner(f)).collect()),
133            Self::Task(fut) => {
134                let f = f.clone();
135                Command::Task(Box::pin(async move { f(fut.await) }))
136            }
137            Self::Navigate { route } => Command::Navigate { route },
138            Self::SaveState { key } => Command::SaveState { key },
139            Self::LoadState { .. } => {
140                // Can't easily map LoadState due to function pointer
141                // In practice, LoadState is usually at the top level
142                Command::None
143            }
144        }
145    }
146}
147
148/// A simple counter state for testing.
149#[derive(Debug, Clone, Default, Serialize, Deserialize)]
150pub struct CounterState {
151    /// Current count
152    pub count: i32,
153}
154
155/// Messages for the counter state.
156#[derive(Debug, Clone)]
157pub enum CounterMessage {
158    /// Increment the counter
159    Increment,
160    /// Decrement the counter
161    Decrement,
162    /// Set the counter to a specific value
163    Set(i32),
164    /// Reset to zero
165    Reset,
166}
167
168impl State for CounterState {
169    type Message = CounterMessage;
170
171    fn update(&mut self, msg: Self::Message) -> Command<Self::Message> {
172        match msg {
173            CounterMessage::Increment => self.count += 1,
174            CounterMessage::Decrement => self.count -= 1,
175            CounterMessage::Set(value) => self.count = value,
176            CounterMessage::Reset => self.count = 0,
177        }
178        Command::None
179    }
180}
181
182/// Type alias for state change subscribers.
183type Subscriber<S> = Box<dyn Fn(&S) + Send + Sync>;
184
185/// Store manages state lifecycle with subscriptions and time-travel debugging.
186pub struct Store<S: State> {
187    state: S,
188    history: Vec<S>,
189    history_index: usize,
190    max_history: usize,
191    subscribers: Vec<Subscriber<S>>,
192}
193
194impl<S: State> Store<S> {
195    /// Create a new store with initial state.
196    pub fn new(initial: S) -> Self {
197        Self {
198            state: initial,
199            history: Vec::new(),
200            history_index: 0,
201            max_history: 100,
202            subscribers: Vec::new(),
203        }
204    }
205
206    /// Create a store with custom history limit.
207    pub fn with_history_limit(initial: S, max_history: usize) -> Self {
208        Self {
209            state: initial,
210            history: Vec::new(),
211            history_index: 0,
212            max_history,
213            subscribers: Vec::new(),
214        }
215    }
216
217    /// Get current state.
218    pub const fn state(&self) -> &S {
219        &self.state
220    }
221
222    /// Dispatch a message to update state.
223    pub fn dispatch(&mut self, msg: S::Message) -> Command<S::Message> {
224        // Save current state to history
225        if self.max_history > 0 {
226            // Truncate future history if we're not at the end
227            if self.history_index < self.history.len() {
228                self.history.truncate(self.history_index);
229            }
230
231            self.history.push(self.state.clone());
232
233            // Limit history size
234            if self.history.len() > self.max_history {
235                self.history.remove(0);
236            } else {
237                self.history_index = self.history.len();
238            }
239        }
240
241        // Update state
242        let cmd = self.state.update(msg);
243
244        // Notify subscribers
245        self.notify_subscribers();
246
247        cmd
248    }
249
250    /// Subscribe to state changes.
251    pub fn subscribe<F>(&mut self, callback: F)
252    where
253        F: Fn(&S) + Send + Sync + 'static,
254    {
255        self.subscribers.push(Box::new(callback));
256    }
257
258    /// Get number of history entries.
259    pub fn history_len(&self) -> usize {
260        self.history.len()
261    }
262
263    /// Can undo to previous state.
264    pub const fn can_undo(&self) -> bool {
265        self.history_index > 0
266    }
267
268    /// Can redo to next state.
269    pub fn can_redo(&self) -> bool {
270        self.history_index < self.history.len()
271    }
272
273    /// Undo to previous state.
274    pub fn undo(&mut self) -> bool {
275        if self.can_undo() {
276            // If we're at the end, save current state first
277            if self.history_index == self.history.len() {
278                self.history.push(self.state.clone());
279            }
280
281            self.history_index -= 1;
282            self.state = self.history[self.history_index].clone();
283            self.notify_subscribers();
284            true
285        } else {
286            false
287        }
288    }
289
290    /// Redo to next state.
291    pub fn redo(&mut self) -> bool {
292        if self.history_index < self.history.len().saturating_sub(1) {
293            self.history_index += 1;
294            self.state = self.history[self.history_index].clone();
295            self.notify_subscribers();
296            true
297        } else {
298            false
299        }
300    }
301
302    /// Jump to a specific point in history.
303    pub fn jump_to(&mut self, index: usize) -> bool {
304        if index < self.history.len() {
305            self.history_index = index;
306            self.state = self.history[index].clone();
307            self.notify_subscribers();
308            true
309        } else {
310            false
311        }
312    }
313
314    /// Clear history.
315    pub fn clear_history(&mut self) {
316        self.history.clear();
317        self.history_index = 0;
318    }
319
320    fn notify_subscribers(&self) {
321        for subscriber in &self.subscribers {
322            subscriber(&self.state);
323        }
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn test_counter_increment() {
333        let mut state = CounterState::default();
334        state.update(CounterMessage::Increment);
335        assert_eq!(state.count, 1);
336    }
337
338    #[test]
339    fn test_counter_decrement() {
340        let mut state = CounterState { count: 5 };
341        state.update(CounterMessage::Decrement);
342        assert_eq!(state.count, 4);
343    }
344
345    #[test]
346    fn test_counter_set() {
347        let mut state = CounterState::default();
348        state.update(CounterMessage::Set(42));
349        assert_eq!(state.count, 42);
350    }
351
352    #[test]
353    fn test_counter_reset() {
354        let mut state = CounterState { count: 100 };
355        state.update(CounterMessage::Reset);
356        assert_eq!(state.count, 0);
357    }
358
359    #[test]
360    fn test_command_none() {
361        let cmd: Command<()> = Command::None;
362        assert!(cmd.is_none());
363    }
364
365    #[test]
366    fn test_command_default() {
367        let cmd: Command<()> = Command::default();
368        assert!(cmd.is_none());
369    }
370
371    #[test]
372    fn test_command_batch() {
373        let cmd: Command<i32> = Command::batch([
374            Command::Navigate {
375                route: "/a".to_string(),
376            },
377            Command::Navigate {
378                route: "/b".to_string(),
379            },
380        ]);
381        assert!(!cmd.is_none());
382        if let Command::Batch(cmds) = cmd {
383            assert_eq!(cmds.len(), 2);
384        } else {
385            panic!("Expected Batch command");
386        }
387    }
388
389    #[test]
390    fn test_command_navigate() {
391        let cmd: Command<()> = Command::Navigate {
392            route: "/home".to_string(),
393        };
394        if let Command::Navigate { route } = cmd {
395            assert_eq!(route, "/home");
396        } else {
397            panic!("Expected Navigate command");
398        }
399    }
400
401    #[test]
402    fn test_command_save_state() {
403        let cmd: Command<()> = Command::SaveState {
404            key: "app_state".to_string(),
405        };
406        if let Command::SaveState { key } = cmd {
407            assert_eq!(key, "app_state");
408        } else {
409            panic!("Expected SaveState command");
410        }
411    }
412
413    #[test]
414    fn test_counter_serialization() {
415        let state = CounterState { count: 42 };
416        let json = serde_json::to_string(&state).unwrap();
417        let loaded: CounterState = serde_json::from_str(&json).unwrap();
418        assert_eq!(loaded.count, 42);
419    }
420
421    #[test]
422    fn test_command_map() {
423        let cmd: Command<i32> = Command::Navigate {
424            route: "/test".to_string(),
425        };
426        let mapped: Command<String> = cmd.map(|_i| "mapped".to_string());
427
428        if let Command::Navigate { route } = mapped {
429            assert_eq!(route, "/test");
430        } else {
431            panic!("Expected Navigate command after map");
432        }
433    }
434
435    #[test]
436    fn test_command_map_none() {
437        let cmd: Command<i32> = Command::None;
438        let mapped: Command<String> = cmd.map(|i| i.to_string());
439        assert!(mapped.is_none());
440    }
441
442    #[test]
443    fn test_command_batch_map() {
444        let cmd: Command<i32> = Command::batch([
445            Command::SaveState {
446                key: "key1".to_string(),
447            },
448            Command::SaveState {
449                key: "key2".to_string(),
450            },
451        ]);
452
453        let mapped: Command<String> = cmd.map(|i| format!("val_{i}"));
454
455        if let Command::Batch(cmds) = mapped {
456            assert_eq!(cmds.len(), 2);
457        } else {
458            panic!("Expected Batch command after map");
459        }
460    }
461
462    // =========================================================================
463    // Store Tests
464    // =========================================================================
465
466    #[test]
467    fn test_store_new() {
468        let store = Store::new(CounterState::default());
469        assert_eq!(store.state().count, 0);
470    }
471
472    #[test]
473    fn test_store_dispatch() {
474        let mut store = Store::new(CounterState::default());
475        store.dispatch(CounterMessage::Increment);
476        assert_eq!(store.state().count, 1);
477    }
478
479    #[test]
480    fn test_store_history() {
481        let mut store = Store::new(CounterState::default());
482
483        store.dispatch(CounterMessage::Increment);
484        store.dispatch(CounterMessage::Increment);
485        store.dispatch(CounterMessage::Increment);
486
487        assert_eq!(store.state().count, 3);
488        assert_eq!(store.history_len(), 3);
489    }
490
491    #[test]
492    fn test_store_undo() {
493        let mut store = Store::new(CounterState::default());
494
495        store.dispatch(CounterMessage::Increment);
496        store.dispatch(CounterMessage::Increment);
497        assert_eq!(store.state().count, 2);
498
499        assert!(store.can_undo());
500        assert!(store.undo());
501        assert_eq!(store.state().count, 1);
502
503        assert!(store.undo());
504        assert_eq!(store.state().count, 0);
505    }
506
507    #[test]
508    fn test_store_redo() {
509        let mut store = Store::new(CounterState::default());
510
511        store.dispatch(CounterMessage::Increment);
512        store.dispatch(CounterMessage::Increment);
513        store.undo();
514        store.undo();
515
516        assert_eq!(store.state().count, 0);
517        assert!(store.can_redo());
518
519        assert!(store.redo());
520        assert_eq!(store.state().count, 1);
521
522        assert!(store.redo());
523        assert_eq!(store.state().count, 2);
524    }
525
526    #[test]
527    fn test_store_undo_at_start() {
528        let mut store = Store::new(CounterState::default());
529        assert!(!store.can_undo());
530        assert!(!store.undo());
531    }
532
533    #[test]
534    fn test_store_redo_at_end() {
535        let mut store = Store::new(CounterState::default());
536        store.dispatch(CounterMessage::Increment);
537        assert!(!store.can_redo());
538        assert!(!store.redo());
539    }
540
541    #[test]
542    fn test_store_history_truncation() {
543        let mut store = Store::new(CounterState::default());
544
545        store.dispatch(CounterMessage::Set(1));
546        store.dispatch(CounterMessage::Set(2));
547        store.dispatch(CounterMessage::Set(3));
548
549        // Undo to 1
550        store.undo();
551        store.undo();
552        assert_eq!(store.state().count, 1);
553
554        // New dispatch should truncate redo history
555        store.dispatch(CounterMessage::Set(10));
556        assert_eq!(store.state().count, 10);
557
558        // Cannot redo to 2 or 3 anymore
559        assert!(!store.redo());
560    }
561
562    #[test]
563    fn test_store_jump_to() {
564        let mut store = Store::new(CounterState::default());
565
566        store.dispatch(CounterMessage::Set(10));
567        store.dispatch(CounterMessage::Set(20));
568        store.dispatch(CounterMessage::Set(30));
569
570        assert!(store.jump_to(0));
571        assert_eq!(store.state().count, 0);
572
573        assert!(store.jump_to(2));
574        assert_eq!(store.state().count, 20);
575    }
576
577    #[test]
578    fn test_store_jump_invalid() {
579        let mut store = Store::new(CounterState::default());
580        store.dispatch(CounterMessage::Increment);
581
582        assert!(!store.jump_to(100));
583    }
584
585    #[test]
586    fn test_store_clear_history() {
587        let mut store = Store::new(CounterState::default());
588
589        store.dispatch(CounterMessage::Increment);
590        store.dispatch(CounterMessage::Increment);
591        assert!(store.history_len() > 0);
592
593        store.clear_history();
594        assert_eq!(store.history_len(), 0);
595        assert!(!store.can_undo());
596    }
597
598    #[test]
599    fn test_store_with_history_limit() {
600        let mut store = Store::with_history_limit(CounterState::default(), 3);
601
602        for i in 1..=10 {
603            store.dispatch(CounterMessage::Set(i));
604        }
605
606        // History should be capped at 3
607        assert!(store.history_len() <= 3);
608    }
609
610    #[test]
611    fn test_store_subscribe() {
612        use std::sync::atomic::{AtomicI32, Ordering};
613        use std::sync::Arc;
614
615        let call_count = Arc::new(AtomicI32::new(0));
616        let call_count_clone = call_count.clone();
617
618        let mut store = Store::new(CounterState::default());
619        store.subscribe(move |_| {
620            call_count_clone.fetch_add(1, Ordering::SeqCst);
621        });
622
623        store.dispatch(CounterMessage::Increment);
624        store.dispatch(CounterMessage::Increment);
625
626        assert_eq!(call_count.load(Ordering::SeqCst), 2);
627    }
628
629    #[test]
630    fn test_store_no_history() {
631        let mut store = Store::with_history_limit(CounterState::default(), 0);
632
633        store.dispatch(CounterMessage::Increment);
634        store.dispatch(CounterMessage::Increment);
635
636        assert_eq!(store.history_len(), 0);
637        assert!(!store.can_undo());
638    }
639}