tui_dispatch_core/
store.rs

1//! Centralized state store with reducer pattern
2
3use crate::Action;
4use std::marker::PhantomData;
5
6/// A reducer function that handles actions and mutates state
7///
8/// Returns `true` if the state changed and a re-render is needed.
9pub type Reducer<S, A> = fn(&mut S, A) -> bool;
10
11/// Compose a reducer by routing actions to focused handlers.
12///
13/// # When to Use
14///
15/// For most reducers, a flat `match` is simpler and clearer. Use this macro when:
16/// - Your reducer exceeds ~500 lines and splitting improves organization
17/// - You have **context-aware routing** (e.g., vim normal vs command mode)
18/// - Handlers live in **separate modules** and you want clean composition
19///
20/// # Syntax
21///
22/// ```ignore
23/// reducer_compose!(state, action, {
24///     // Arms are tried in order, first match wins
25///     category "name" => handler,      // Route by action category
26///     Action::Specific => handler,     // Route by pattern match
27///     _ => fallback_handler,           // Catch-all (required last)
28/// })
29///
30/// // With context (e.g., for modal/mode-aware routing):
31/// reducer_compose!(state, action, context, {
32///     context Mode::Command => handle_command,  // Route by context value
33///     category "nav" => handle_nav,
34///     _ => handle_default,
35/// })
36/// ```
37///
38/// # Arm Types
39///
40/// **`category "name" => handler`** - Routes actions where
41/// `ActionCategory::category(&action) == Some("name")`. Requires
42/// `#[action(infer_categories)]` on your action enum.
43///
44/// **`context Value => handler`** - Routes when the context expression equals
45/// `Value`. Only available in the 4-argument form.
46///
47/// **`Pattern => handler`** - Standard pattern match (e.g., `Action::Quit`,
48/// `Action::Input(_)`).
49///
50/// **`_ => handler`** - Catch-all fallback. Must be last.
51///
52/// # Handler Signature
53///
54/// All handlers must have the same signature:
55/// ```ignore
56/// fn handler(state: &mut S, action: A) -> R
57/// ```
58/// Where `R` is typically `bool` or `DispatchResult<E>`.
59///
60/// # Category Inference
61///
62/// With `#[action(infer_categories)]`, categories are inferred from action
63/// names by taking the prefix before the verb:
64///
65/// | Action | Verb | Category |
66/// |--------|------|----------|
67/// | `NavScrollUp` | Scroll | `"nav"` |
68/// | `SearchQuerySubmit` | Submit | `"search_query"` |
69/// | `WeatherDidLoad` | Load | `"weather_did"` |
70/// | `Quit` | (none) | `None` |
71///
72/// For predictable categories, use explicit `#[category = "name"]` attributes.
73///
74/// # Example
75///
76/// ```ignore
77/// fn reducer(state: &mut AppState, action: Action, mode: Mode) -> bool {
78///     reducer_compose!(state, action, mode, {
79///         // Command mode gets priority
80///         context Mode::Command => handle_command,
81///         // Then route by category
82///         category "nav" => handle_navigation,
83///         category "search" => handle_search,
84///         // Specific actions
85///         Action::Quit => |_, _| false,
86///         // Everything else
87///         _ => handle_ui,
88///     })
89/// }
90///
91/// fn handle_navigation(state: &mut AppState, action: Action) -> bool {
92///     match action {
93///         Action::NavUp => { state.cursor -= 1; true }
94///         Action::NavDown => { state.cursor += 1; true }
95///         _ => false,
96///     }
97/// }
98/// ```
99#[macro_export]
100macro_rules! reducer_compose {
101    // 3-argument form must come first to prevent $context:expr from matching the braces
102    ($state:expr, $action:expr, { $($arms:tt)+ }) => {{
103        let __state = $state;
104        let __action_input = $action;
105        let __context = ();
106        $crate::reducer_compose!(@accum __state, __action_input, __context; () $($arms)+)
107    }};
108    ($state:expr, $action:expr, $context:expr, { $($arms:tt)+ }) => {{
109        let __state = $state;
110        let __action_input = $action;
111        let __context = $context;
112        $crate::reducer_compose!(@accum __state, __action_input, __context; () $($arms)+)
113    }};
114    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) category $category:expr => $handler:expr, $($rest:tt)+) => {
115        $crate::reducer_compose!(
116            @accum $state, $action, $context;
117            (
118                $($out)*
119                __action if $crate::ActionCategory::category(&__action) == Some($category) => {
120                    ($handler)($state, __action)
121                },
122            )
123            $($rest)+
124        )
125    };
126    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) context $context_value:expr => $handler:expr, $($rest:tt)+) => {
127        $crate::reducer_compose!(
128            @accum $state, $action, $context;
129            (
130                $($out)*
131                __action if $context == $context_value => {
132                    ($handler)($state, __action)
133                },
134            )
135            $($rest)+
136        )
137    };
138    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) _ => $handler:expr, $($rest:tt)+) => {
139        $crate::reducer_compose!(
140            @accum $state, $action, $context;
141            (
142                $($out)*
143                __action => {
144                    ($handler)($state, __action)
145                },
146            )
147            $($rest)+
148        )
149    };
150    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) $pattern:pat $(if $guard:expr)? => $handler:expr, $($rest:tt)+) => {
151        $crate::reducer_compose!(
152            @accum $state, $action, $context;
153            (
154                $($out)*
155                __action @ $pattern $(if $guard)? => {
156                    ($handler)($state, __action)
157                },
158            )
159            $($rest)+
160        )
161    };
162    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) category $category:expr => $handler:expr $(,)?) => {
163        match $action {
164            $($out)*
165            __action if $crate::ActionCategory::category(&__action) == Some($category) => {
166                ($handler)($state, __action)
167            }
168        }
169    };
170    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) context $context_value:expr => $handler:expr $(,)?) => {
171        match $action {
172            $($out)*
173            __action if $context == $context_value => {
174                ($handler)($state, __action)
175            }
176        }
177    };
178    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) _ => $handler:expr $(,)?) => {
179        match $action {
180            $($out)*
181            __action => {
182                ($handler)($state, __action)
183            }
184        }
185    };
186    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) $pattern:pat $(if $guard:expr)? => $handler:expr $(,)?) => {
187        match $action {
188            $($out)*
189            __action @ $pattern $(if $guard)? => {
190                ($handler)($state, __action)
191            }
192        }
193    };
194}
195
196/// Centralized state store with Redux-like reducer pattern
197///
198/// The store holds the application state and provides a single point
199/// for state mutations through the `dispatch` method.
200///
201/// # Type Parameters
202/// * `S` - The application state type
203/// * `A` - The action type (must implement `Action`)
204///
205/// # Example
206/// ```ignore
207/// #[derive(Default)]
208/// struct AppState {
209///     counter: i32,
210/// }
211///
212/// #[derive(Action, Clone, Debug)]
213/// enum MyAction {
214///     Increment,
215///     Decrement,
216/// }
217///
218/// fn reducer(state: &mut AppState, action: MyAction) -> bool {
219///     match action {
220///         MyAction::Increment => {
221///             state.counter += 1;
222///             true
223///         }
224///         MyAction::Decrement => {
225///             state.counter -= 1;
226///             true
227///         }
228///     }
229/// }
230///
231/// let mut store = Store::new(AppState::default(), reducer);
232/// store.dispatch(MyAction::Increment);
233/// assert_eq!(store.state().counter, 1);
234/// ```
235pub struct Store<S, A: Action> {
236    state: S,
237    reducer: Reducer<S, A>,
238    _marker: PhantomData<A>,
239}
240
241impl<S, A: Action> Store<S, A> {
242    /// Create a new store with initial state and reducer
243    pub fn new(state: S, reducer: Reducer<S, A>) -> Self {
244        Self {
245            state,
246            reducer,
247            _marker: PhantomData,
248        }
249    }
250
251    /// Dispatch an action to the store
252    ///
253    /// The reducer will be called with the current state and action.
254    /// Returns `true` if the state changed and a re-render is needed.
255    pub fn dispatch(&mut self, action: A) -> bool {
256        (self.reducer)(&mut self.state, action)
257    }
258
259    /// Get a reference to the current state
260    pub fn state(&self) -> &S {
261        &self.state
262    }
263
264    /// Get a mutable reference to the state
265    ///
266    /// Use this sparingly - prefer dispatching actions for state changes.
267    /// This is useful for initializing state or for cases where the
268    /// action pattern doesn't fit well.
269    pub fn state_mut(&mut self) -> &mut S {
270        &mut self.state
271    }
272}
273
274/// Store with middleware support
275///
276/// Wraps a `Store` and allows middleware to intercept actions
277/// before and after they are processed by the reducer.
278pub struct StoreWithMiddleware<S, A: Action, M: Middleware<A>> {
279    store: Store<S, A>,
280    middleware: M,
281}
282
283impl<S, A: Action, M: Middleware<A>> StoreWithMiddleware<S, A, M> {
284    /// Create a new store with middleware
285    pub fn new(state: S, reducer: Reducer<S, A>, middleware: M) -> Self {
286        Self {
287            store: Store::new(state, reducer),
288            middleware,
289        }
290    }
291
292    /// Dispatch an action through middleware and store
293    pub fn dispatch(&mut self, action: A) -> bool {
294        self.middleware.before(&action);
295        let changed = self.store.dispatch(action.clone());
296        self.middleware.after(&action, changed);
297        changed
298    }
299
300    /// Get a reference to the current state
301    pub fn state(&self) -> &S {
302        self.store.state()
303    }
304
305    /// Get a mutable reference to the state
306    pub fn state_mut(&mut self) -> &mut S {
307        self.store.state_mut()
308    }
309
310    /// Get a reference to the middleware
311    pub fn middleware(&self) -> &M {
312        &self.middleware
313    }
314
315    /// Get a mutable reference to the middleware
316    pub fn middleware_mut(&mut self) -> &mut M {
317        &mut self.middleware
318    }
319}
320
321/// Middleware trait for intercepting actions
322///
323/// Implement this trait to add logging, persistence, or other
324/// cross-cutting concerns to your store.
325pub trait Middleware<A: Action> {
326    /// Called before the action is dispatched to the reducer
327    fn before(&mut self, action: &A);
328
329    /// Called after the action is processed by the reducer
330    fn after(&mut self, action: &A, state_changed: bool);
331}
332
333/// A no-op middleware that does nothing
334#[derive(Debug, Clone, Copy, Default)]
335pub struct NoopMiddleware;
336
337impl<A: Action> Middleware<A> for NoopMiddleware {
338    fn before(&mut self, _action: &A) {}
339    fn after(&mut self, _action: &A, _state_changed: bool) {}
340}
341
342/// Middleware that logs actions (for debugging)
343#[derive(Debug, Clone, Default)]
344pub struct LoggingMiddleware {
345    /// Whether to log before dispatch
346    pub log_before: bool,
347    /// Whether to log after dispatch
348    pub log_after: bool,
349}
350
351impl LoggingMiddleware {
352    /// Create a new logging middleware with default settings (log after only)
353    pub fn new() -> Self {
354        Self {
355            log_before: false,
356            log_after: true,
357        }
358    }
359
360    /// Create a logging middleware that logs both before and after
361    pub fn verbose() -> Self {
362        Self {
363            log_before: true,
364            log_after: true,
365        }
366    }
367}
368
369impl<A: Action> Middleware<A> for LoggingMiddleware {
370    fn before(&mut self, action: &A) {
371        if self.log_before {
372            tracing::debug!(action = %action.name(), "Dispatching action");
373        }
374    }
375
376    fn after(&mut self, action: &A, state_changed: bool) {
377        if self.log_after {
378            tracing::debug!(
379                action = %action.name(),
380                state_changed = state_changed,
381                "Action processed"
382            );
383        }
384    }
385}
386
387/// Compose multiple middleware into a single middleware
388pub struct ComposedMiddleware<A: Action> {
389    middlewares: Vec<Box<dyn Middleware<A>>>,
390}
391
392impl<A: Action> std::fmt::Debug for ComposedMiddleware<A> {
393    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394        f.debug_struct("ComposedMiddleware")
395            .field("middlewares_count", &self.middlewares.len())
396            .finish()
397    }
398}
399
400impl<A: Action> Default for ComposedMiddleware<A> {
401    fn default() -> Self {
402        Self::new()
403    }
404}
405
406impl<A: Action> ComposedMiddleware<A> {
407    /// Create a new composed middleware
408    pub fn new() -> Self {
409        Self {
410            middlewares: Vec::new(),
411        }
412    }
413
414    /// Add a middleware to the composition
415    pub fn add<M: Middleware<A> + 'static>(&mut self, middleware: M) {
416        self.middlewares.push(Box::new(middleware));
417    }
418}
419
420impl<A: Action> Middleware<A> for ComposedMiddleware<A> {
421    fn before(&mut self, action: &A) {
422        for middleware in &mut self.middlewares {
423            middleware.before(action);
424        }
425    }
426
427    fn after(&mut self, action: &A, state_changed: bool) {
428        // Call in reverse order for proper nesting
429        for middleware in self.middlewares.iter_mut().rev() {
430            middleware.after(action, state_changed);
431        }
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::ActionCategory;
439
440    #[derive(Default)]
441    struct TestState {
442        counter: i32,
443    }
444
445    #[derive(Clone, Debug)]
446    enum TestAction {
447        Increment,
448        Decrement,
449        NoOp,
450    }
451
452    impl Action for TestAction {
453        fn name(&self) -> &'static str {
454            match self {
455                TestAction::Increment => "Increment",
456                TestAction::Decrement => "Decrement",
457                TestAction::NoOp => "NoOp",
458            }
459        }
460    }
461
462    fn test_reducer(state: &mut TestState, action: TestAction) -> bool {
463        match action {
464            TestAction::Increment => {
465                state.counter += 1;
466                true
467            }
468            TestAction::Decrement => {
469                state.counter -= 1;
470                true
471            }
472            TestAction::NoOp => false,
473        }
474    }
475
476    #[test]
477    fn test_store_dispatch() {
478        let mut store = Store::new(TestState::default(), test_reducer);
479
480        assert!(store.dispatch(TestAction::Increment));
481        assert_eq!(store.state().counter, 1);
482
483        assert!(store.dispatch(TestAction::Increment));
484        assert_eq!(store.state().counter, 2);
485
486        assert!(store.dispatch(TestAction::Decrement));
487        assert_eq!(store.state().counter, 1);
488    }
489
490    #[test]
491    fn test_store_noop() {
492        let mut store = Store::new(TestState::default(), test_reducer);
493
494        assert!(!store.dispatch(TestAction::NoOp));
495        assert_eq!(store.state().counter, 0);
496    }
497
498    #[test]
499    fn test_store_state_mut() {
500        let mut store = Store::new(TestState::default(), test_reducer);
501
502        store.state_mut().counter = 100;
503        assert_eq!(store.state().counter, 100);
504    }
505
506    #[derive(Default)]
507    struct CountingMiddleware {
508        before_count: usize,
509        after_count: usize,
510    }
511
512    impl<A: Action> Middleware<A> for CountingMiddleware {
513        fn before(&mut self, _action: &A) {
514            self.before_count += 1;
515        }
516
517        fn after(&mut self, _action: &A, _state_changed: bool) {
518            self.after_count += 1;
519        }
520    }
521
522    #[test]
523    fn test_store_with_middleware() {
524        let mut store = StoreWithMiddleware::new(
525            TestState::default(),
526            test_reducer,
527            CountingMiddleware::default(),
528        );
529
530        store.dispatch(TestAction::Increment);
531        store.dispatch(TestAction::Increment);
532
533        assert_eq!(store.middleware().before_count, 2);
534        assert_eq!(store.middleware().after_count, 2);
535        assert_eq!(store.state().counter, 2);
536    }
537
538    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
539    enum ComposeContext {
540        Default,
541        Command,
542    }
543
544    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
545    enum ComposeCategory {
546        Nav,
547        Search,
548        Uncategorized,
549    }
550
551    #[derive(Clone, Debug)]
552    enum ComposeAction {
553        NavUp,
554        Search,
555        Other,
556    }
557
558    impl Action for ComposeAction {
559        fn name(&self) -> &'static str {
560            match self {
561                ComposeAction::NavUp => "NavUp",
562                ComposeAction::Search => "Search",
563                ComposeAction::Other => "Other",
564            }
565        }
566    }
567
568    impl ActionCategory for ComposeAction {
569        type Category = ComposeCategory;
570
571        fn category(&self) -> Option<&'static str> {
572            match self {
573                ComposeAction::NavUp => Some("nav"),
574                ComposeAction::Search => Some("search"),
575                ComposeAction::Other => None,
576            }
577        }
578
579        fn category_enum(&self) -> Self::Category {
580            match self {
581                ComposeAction::NavUp => ComposeCategory::Nav,
582                ComposeAction::Search => ComposeCategory::Search,
583                ComposeAction::Other => ComposeCategory::Uncategorized,
584            }
585        }
586    }
587
588    fn handle_nav(state: &mut usize, _action: ComposeAction) -> &'static str {
589        *state += 1;
590        "nav"
591    }
592
593    fn handle_command(state: &mut usize, _action: ComposeAction) -> &'static str {
594        *state += 10;
595        "command"
596    }
597
598    fn handle_search(state: &mut usize, _action: ComposeAction) -> &'static str {
599        *state += 100;
600        "search"
601    }
602
603    fn handle_default(state: &mut usize, _action: ComposeAction) -> &'static str {
604        *state += 1000;
605        "default"
606    }
607
608    fn composed_reducer(
609        state: &mut usize,
610        action: ComposeAction,
611        context: ComposeContext,
612    ) -> &'static str {
613        crate::reducer_compose!(state, action, context, {
614            category "nav" => handle_nav,
615            context ComposeContext::Command => handle_command,
616            ComposeAction::Search => handle_search,
617            _ => handle_default,
618        })
619    }
620
621    #[test]
622    fn test_reducer_compose_routes_category() {
623        let mut state = 0;
624        let result = composed_reducer(&mut state, ComposeAction::NavUp, ComposeContext::Command);
625        assert_eq!(result, "nav");
626        assert_eq!(state, 1);
627    }
628
629    #[test]
630    fn test_reducer_compose_routes_context() {
631        let mut state = 0;
632        let result = composed_reducer(&mut state, ComposeAction::Other, ComposeContext::Command);
633        assert_eq!(result, "command");
634        assert_eq!(state, 10);
635    }
636
637    #[test]
638    fn test_reducer_compose_routes_pattern() {
639        let mut state = 0;
640        let result = composed_reducer(&mut state, ComposeAction::Search, ComposeContext::Default);
641        assert_eq!(result, "search");
642        assert_eq!(state, 100);
643    }
644
645    #[test]
646    fn test_reducer_compose_routes_fallback() {
647        let mut state = 0;
648        let result = composed_reducer(&mut state, ComposeAction::Other, ComposeContext::Default);
649        assert_eq!(result, "default");
650        assert_eq!(state, 1000);
651    }
652
653    // Test 3-argument form (no context)
654    fn composed_reducer_no_context(state: &mut usize, action: ComposeAction) -> &'static str {
655        crate::reducer_compose!(state, action, {
656            category "nav" => handle_nav,
657            ComposeAction::Search => handle_search,
658            _ => handle_default,
659        })
660    }
661
662    #[test]
663    fn test_reducer_compose_3arg_category() {
664        let mut state = 0;
665        let result = composed_reducer_no_context(&mut state, ComposeAction::NavUp);
666        assert_eq!(result, "nav");
667        assert_eq!(state, 1);
668    }
669
670    #[test]
671    fn test_reducer_compose_3arg_pattern() {
672        let mut state = 0;
673        let result = composed_reducer_no_context(&mut state, ComposeAction::Search);
674        assert_eq!(result, "search");
675        assert_eq!(state, 100);
676    }
677
678    #[test]
679    fn test_reducer_compose_3arg_fallback() {
680        let mut state = 0;
681        let result = composed_reducer_no_context(&mut state, ComposeAction::Other);
682        assert_eq!(result, "default");
683        assert_eq!(state, 1000);
684    }
685}