Skip to main content

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/// Default maximum nested middleware dispatch depth.
12pub(crate) const DEFAULT_MAX_DISPATCH_DEPTH: usize = 16;
13/// Default maximum number of actions processed per top-level dispatch.
14pub(crate) const DEFAULT_MAX_DISPATCH_ACTIONS: usize = 10_000;
15
16/// Configurable limits for middleware dispatch.
17///
18/// Both limits should be at least `1` for dispatch to make progress.
19/// If either value is `0`, all dispatch attempts fail with [`DispatchError`].
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct DispatchLimits {
22    /// Maximum nested dispatch depth from middleware injection.
23    pub max_depth: usize,
24    /// Maximum attempted actions during a single top-level dispatch.
25    ///
26    /// This includes actions cancelled by `Middleware::before`.
27    pub max_actions: usize,
28}
29
30impl Default for DispatchLimits {
31    fn default() -> Self {
32        Self {
33            max_depth: DEFAULT_MAX_DISPATCH_DEPTH,
34            max_actions: DEFAULT_MAX_DISPATCH_ACTIONS,
35        }
36    }
37}
38
39/// Error returned when middleware dispatch exceeds configured limits.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum DispatchError {
42    /// Nested middleware injection exceeded `DispatchLimits::max_depth`.
43    DepthExceeded {
44        max_depth: usize,
45        action: &'static str,
46    },
47    /// Processed actions exceeded `DispatchLimits::max_actions`.
48    ActionBudgetExceeded {
49        max_actions: usize,
50        processed: usize,
51        action: &'static str,
52    },
53}
54
55impl std::fmt::Display for DispatchError {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            DispatchError::DepthExceeded { max_depth, action } => write!(
59                f,
60                "middleware dispatch depth limit exceeded (max_depth={max_depth}, action={action})"
61            ),
62            DispatchError::ActionBudgetExceeded {
63                max_actions,
64                processed,
65                action,
66            } => write!(
67                f,
68                "middleware dispatch action budget exceeded (max_actions={max_actions}, processed={processed}, action={action})"
69            ),
70        }
71    }
72}
73
74impl std::error::Error for DispatchError {}
75
76pub(crate) fn check_dispatch_limits(
77    limits: DispatchLimits,
78    dispatch_depth: usize,
79    processed: usize,
80    action: &'static str,
81) -> Result<(), DispatchError> {
82    if dispatch_depth >= limits.max_depth {
83        return Err(DispatchError::DepthExceeded {
84            max_depth: limits.max_depth,
85            action,
86        });
87    }
88
89    if processed >= limits.max_actions {
90        return Err(DispatchError::ActionBudgetExceeded {
91            max_actions: limits.max_actions,
92            processed,
93            action,
94        });
95    }
96
97    Ok(())
98}
99
100#[inline]
101pub(crate) fn debug_assert_valid_dispatch_limits(limits: DispatchLimits) {
102    debug_assert!(
103        limits.max_depth >= 1 && limits.max_actions >= 1,
104        "DispatchLimits requires max_depth >= 1 and max_actions >= 1"
105    );
106}
107
108pub(crate) trait MiddlewareDispatchDriver<A: Action> {
109    type Output;
110
111    fn before(&mut self, action: &A) -> bool;
112    fn reduce(&mut self, action: A) -> Self::Output;
113    /// Called only when the root action is cancelled in `before()`.
114    ///
115    /// Cancelled non-root actions are discarded because they merge as identity
116    /// values into their parent results.
117    fn cancelled_output(&mut self) -> Self::Output;
118    fn after(&mut self, action: &A, result: &Self::Output) -> Vec<A>;
119    fn merge_child(&mut self, parent: &mut Self::Output, child: Self::Output);
120}
121
122enum DispatchFrame<A: Action, O> {
123    Pending(A),
124    Entered {
125        result: O,
126        injected: std::vec::IntoIter<A>,
127    },
128}
129
130pub(crate) fn run_iterative_middleware_dispatch<A, D>(
131    limits: DispatchLimits,
132    action: A,
133    driver: &mut D,
134) -> Result<D::Output, DispatchError>
135where
136    A: Action,
137    D: MiddlewareDispatchDriver<A>,
138{
139    let mut processed = 0usize;
140    let mut stack = vec![DispatchFrame::<A, D::Output>::Pending(action)];
141
142    while let Some(frame) = stack.pop() {
143        match frame {
144            DispatchFrame::Pending(action) => {
145                let depth = stack.len();
146                check_dispatch_limits(limits, depth, processed, action.name())?;
147                processed += 1;
148
149                if !driver.before(&action) {
150                    if !stack.is_empty() {
151                        continue;
152                    }
153                    return Ok(driver.cancelled_output());
154                }
155
156                let result = driver.reduce(action.clone());
157                let injected = driver.after(&action, &result).into_iter();
158                stack.push(DispatchFrame::Entered { result, injected });
159            }
160            DispatchFrame::Entered {
161                result,
162                mut injected,
163            } => {
164                if let Some(injected_action) = injected.next() {
165                    stack.push(DispatchFrame::Entered { result, injected });
166                    stack.push(DispatchFrame::Pending(injected_action));
167                    continue;
168                }
169
170                if let Some(DispatchFrame::Entered {
171                    result: parent_result,
172                    ..
173                }) = stack.last_mut()
174                {
175                    driver.merge_child(parent_result, result);
176                    continue;
177                }
178
179                return Ok(result);
180            }
181        }
182    }
183
184    unreachable!("dispatch stack should not drain before a root result is returned")
185}
186
187/// Compose a reducer by routing actions to focused handlers.
188///
189/// # When to Use
190///
191/// For most reducers, a flat `match` is simpler and clearer. Use this macro when:
192/// - Your reducer exceeds ~500 lines and splitting improves organization
193/// - You have **context-aware routing** (e.g., vim normal vs command mode)
194/// - Handlers live in **separate modules** and you want clean composition
195///
196/// # Syntax
197///
198/// ```text
199/// reducer_compose!(state, action, {
200///     // Arms are tried in order, first match wins
201///     category "name" => handler,      // Route by action category
202///     Action::Specific => handler,     // Route by pattern match
203///     _ => fallback_handler,           // Catch-all (required last)
204/// })
205///
206/// // With context (e.g., for modal/mode-aware routing):
207/// reducer_compose!(state, action, context, {
208///     context Mode::Command => handle_command,  // Route by context value
209///     category "nav" => handle_nav,
210///     _ => handle_default,
211/// })
212/// ```
213///
214/// # Arm Types
215///
216/// **`category "name" => handler`** - Routes actions where
217/// `ActionCategory::category(&action) == Some("name")`. Requires
218/// `#[action(infer_categories)]` on your action enum.
219///
220/// **`context Value => handler`** - Routes when the context expression equals
221/// `Value`. Only available in the 4-argument form.
222///
223/// **`Pattern => handler`** - Standard pattern match (e.g., `Action::Quit`,
224/// `Action::Input(_)`).
225///
226/// **`_ => handler`** - Catch-all fallback. Must be last.
227///
228/// # Handler Signature
229///
230/// All handlers must have the same signature:
231/// ```text
232/// fn handler(state: &mut S, action: A) -> R
233/// ```
234/// Where `R` is typically `bool` or `ReducerResult<E>`.
235///
236/// # Category Inference
237///
238/// With `#[action(infer_categories)]`, categories are inferred from action
239/// names by taking the prefix before the verb:
240///
241/// | Action | Verb | Category |
242/// |--------|------|----------|
243/// | `NavScrollUp` | Scroll | `"nav"` |
244/// | `SearchQuerySubmit` | Submit | `"search_query"` |
245/// | `WeatherDidLoad` | Did | `"weather"` |
246/// | `Quit` | (none) | `None` |
247///
248/// For predictable categories, use explicit `#[category = "name"]` attributes.
249///
250/// # Example
251///
252/// ```ignore
253/// fn reducer(state: &mut AppState, action: Action, mode: Mode) -> bool {
254///     reducer_compose!(state, action, mode, {
255///         // Command mode gets priority
256///         context Mode::Command => handle_command,
257///         // Then route by category
258///         category "nav" => handle_navigation,
259///         category "search" => handle_search,
260///         // Specific actions
261///         Action::Quit => |_, _| false,
262///         // Everything else
263///         _ => handle_ui,
264///     })
265/// }
266///
267/// fn handle_navigation(state: &mut AppState, action: Action) -> bool {
268///     match action {
269///         Action::NavUp => { state.cursor -= 1; true }
270///         Action::NavDown => { state.cursor += 1; true }
271///         _ => false,
272///     }
273/// }
274/// ```
275#[macro_export]
276macro_rules! reducer_compose {
277    // 3-argument form must come first to prevent $context:expr from matching the braces
278    ($state:expr, $action:expr, { $($arms:tt)+ }) => {{
279        let __state = $state;
280        let __action_input = $action;
281        let __context = ();
282        $crate::reducer_compose!(@accum __state, __action_input, __context; () $($arms)+)
283    }};
284    ($state:expr, $action:expr, $context:expr, { $($arms:tt)+ }) => {{
285        let __state = $state;
286        let __action_input = $action;
287        let __context = $context;
288        $crate::reducer_compose!(@accum __state, __action_input, __context; () $($arms)+)
289    }};
290    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) category $category:expr => $handler:expr, $($rest:tt)+) => {
291        $crate::reducer_compose!(
292            @accum $state, $action, $context;
293            (
294                $($out)*
295                __action if $crate::ActionCategory::category(&__action) == Some($category) => {
296                    ($handler)($state, __action)
297                },
298            )
299            $($rest)+
300        )
301    };
302    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) context $context_value:expr => $handler:expr, $($rest:tt)+) => {
303        $crate::reducer_compose!(
304            @accum $state, $action, $context;
305            (
306                $($out)*
307                __action if $context == $context_value => {
308                    ($handler)($state, __action)
309                },
310            )
311            $($rest)+
312        )
313    };
314    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) _ => $handler:expr, $($rest:tt)+) => {
315        $crate::reducer_compose!(
316            @accum $state, $action, $context;
317            (
318                $($out)*
319                __action => {
320                    ($handler)($state, __action)
321                },
322            )
323            $($rest)+
324        )
325    };
326    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) $pattern:pat $(if $guard:expr)? => $handler:expr, $($rest:tt)+) => {
327        $crate::reducer_compose!(
328            @accum $state, $action, $context;
329            (
330                $($out)*
331                __action @ $pattern $(if $guard)? => {
332                    ($handler)($state, __action)
333                },
334            )
335            $($rest)+
336        )
337    };
338    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) category $category:expr => $handler:expr $(,)?) => {
339        match $action {
340            $($out)*
341            __action if $crate::ActionCategory::category(&__action) == Some($category) => {
342                ($handler)($state, __action)
343            }
344        }
345    };
346    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) context $context_value:expr => $handler:expr $(,)?) => {
347        match $action {
348            $($out)*
349            __action if $context == $context_value => {
350                ($handler)($state, __action)
351            }
352        }
353    };
354    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) _ => $handler:expr $(,)?) => {
355        match $action {
356            $($out)*
357            __action => {
358                ($handler)($state, __action)
359            }
360        }
361    };
362    (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) $pattern:pat $(if $guard:expr)? => $handler:expr $(,)?) => {
363        match $action {
364            $($out)*
365            __action @ $pattern $(if $guard)? => {
366                ($handler)($state, __action)
367            }
368        }
369    };
370}
371
372/// Centralized state store with Redux-like reducer pattern
373///
374/// The store holds the application state and provides a single point
375/// for state mutations through the `dispatch` method.
376///
377/// # Type Parameters
378/// * `S` - The application state type
379/// * `A` - The action type (must implement `Action`)
380///
381/// # Example
382/// ```
383/// use tui_dispatch_core::{Action, Store};
384///
385/// #[derive(Clone, Debug)]
386/// enum MyAction { Increment, Decrement }
387///
388/// impl Action for MyAction {
389///     fn name(&self) -> &'static str {
390///         match self {
391///             MyAction::Increment => "Increment",
392///             MyAction::Decrement => "Decrement",
393///         }
394///     }
395/// }
396///
397/// #[derive(Default)]
398/// struct AppState { counter: i32 }
399///
400/// fn reducer(state: &mut AppState, action: MyAction) -> bool {
401///     match action {
402///         MyAction::Increment => { state.counter += 1; true }
403///         MyAction::Decrement => { state.counter -= 1; true }
404///     }
405/// }
406///
407/// let mut store = Store::new(AppState::default(), reducer);
408/// store.dispatch(MyAction::Increment);
409/// assert_eq!(store.state().counter, 1);
410/// ```
411pub struct Store<S, A: Action> {
412    state: S,
413    reducer: Reducer<S, A>,
414    _marker: PhantomData<A>,
415}
416
417impl<S, A: Action> Store<S, A> {
418    /// Create a new store with initial state and reducer
419    pub fn new(state: S, reducer: Reducer<S, A>) -> Self {
420        Self {
421            state,
422            reducer,
423            _marker: PhantomData,
424        }
425    }
426
427    /// Dispatch an action to the store
428    ///
429    /// The reducer will be called with the current state and action.
430    /// Returns `true` if the state changed and a re-render is needed.
431    pub fn dispatch(&mut self, action: A) -> bool {
432        (self.reducer)(&mut self.state, action)
433    }
434
435    /// Get a reference to the current state
436    pub fn state(&self) -> &S {
437        &self.state
438    }
439
440    /// Get a mutable reference to the state
441    ///
442    /// Use this sparingly - prefer dispatching actions for state changes.
443    /// This is useful for initializing state or for cases where the
444    /// action pattern doesn't fit well.
445    pub fn state_mut(&mut self) -> &mut S {
446        &mut self.state
447    }
448}
449
450/// Store with middleware support
451///
452/// Wraps a `Store` and allows middleware to intercept actions
453/// before and after they are processed by the reducer.
454pub struct StoreWithMiddleware<S, A: Action, M: Middleware<S, A>> {
455    store: Store<S, A>,
456    middleware: M,
457    dispatch_limits: DispatchLimits,
458}
459
460impl<S, A: Action, M: Middleware<S, A>> StoreWithMiddleware<S, A, M> {
461    /// Create a new store with middleware
462    pub fn new(state: S, reducer: Reducer<S, A>, middleware: M) -> Self {
463        Self {
464            store: Store::new(state, reducer),
465            middleware,
466            dispatch_limits: DispatchLimits::default(),
467        }
468    }
469
470    /// Override middleware dispatch limits.
471    pub fn with_dispatch_limits(mut self, limits: DispatchLimits) -> Self {
472        debug_assert_valid_dispatch_limits(limits);
473        self.dispatch_limits = limits;
474        self
475    }
476
477    /// Current middleware dispatch limits.
478    pub fn dispatch_limits(&self) -> DispatchLimits {
479        self.dispatch_limits
480    }
481
482    /// Dispatch an action through middleware and store
483    ///
484    /// This wraps [`Self::try_dispatch`] and panics if dispatch limits are exceeded.
485    /// Use `try_dispatch` to handle overflow as a typed error.
486    pub fn dispatch(&mut self, action: A) -> bool {
487        self.try_dispatch(action)
488            .unwrap_or_else(|error| panic!("middleware dispatch failed: {error}"))
489    }
490
491    /// Dispatch an action through middleware and store.
492    ///
493    /// The action passes through `middleware.before()` (which can cancel it),
494    /// then the reducer, then `middleware.after()` (which can inject follow-up actions).
495    /// Injected actions go through the full pipeline in depth-first order.
496    ///
497    /// Returns [`DispatchError`] if configured depth or action budget limits are exceeded.
498    ///
499    /// This operation is not transactional: if overflow happens in an injected chain,
500    /// earlier actions in the chain may have already mutated state.
501    ///
502    /// Action budget accounting includes attempted dispatches that are later cancelled by
503    /// `Middleware::before`.
504    pub fn try_dispatch(&mut self, action: A) -> Result<bool, DispatchError> {
505        let mut driver = StoreDispatchDriver {
506            store: &mut self.store,
507            middleware: &mut self.middleware,
508        };
509        run_iterative_middleware_dispatch(self.dispatch_limits, action, &mut driver)
510    }
511
512    /// Get a reference to the current state
513    pub fn state(&self) -> &S {
514        self.store.state()
515    }
516
517    /// Get a mutable reference to the state
518    pub fn state_mut(&mut self) -> &mut S {
519        self.store.state_mut()
520    }
521
522    /// Get a reference to the middleware
523    pub fn middleware(&self) -> &M {
524        &self.middleware
525    }
526
527    /// Get a mutable reference to the middleware
528    pub fn middleware_mut(&mut self) -> &mut M {
529        &mut self.middleware
530    }
531}
532
533struct StoreDispatchDriver<'a, S, A: Action, M: Middleware<S, A>> {
534    store: &'a mut Store<S, A>,
535    middleware: &'a mut M,
536}
537
538impl<S, A: Action, M: Middleware<S, A>> MiddlewareDispatchDriver<A>
539    for StoreDispatchDriver<'_, S, A, M>
540{
541    type Output = bool;
542
543    fn before(&mut self, action: &A) -> bool {
544        self.middleware.before(action, &self.store.state)
545    }
546
547    fn reduce(&mut self, action: A) -> Self::Output {
548        self.store.dispatch(action)
549    }
550
551    fn cancelled_output(&mut self) -> Self::Output {
552        false
553    }
554
555    fn after(&mut self, action: &A, result: &Self::Output) -> Vec<A> {
556        self.middleware.after(action, *result, &self.store.state)
557    }
558
559    fn merge_child(&mut self, parent: &mut Self::Output, child: Self::Output) {
560        *parent |= child;
561    }
562}
563
564/// Middleware trait for intercepting actions
565///
566/// Implement this trait to add logging, persistence, throttling, or other
567/// cross-cutting concerns to your store. Middleware can:
568///
569/// - **Observe**: inspect actions and state (logging, analytics, persistence)
570/// - **Cancel**: return `false` from `before()` to prevent the action from reaching the reducer
571/// - **Inject**: return follow-up actions from `after()` that are dispatched through the full pipeline
572///
573/// # Cancel
574///
575/// Return `false` from `before()` to cancel the action — the reducer is never called and
576/// `after()` is not invoked. Useful for throttling, validation, and auth guards.
577///
578/// # Inject
579///
580/// Return actions from `after()` to trigger follow-up dispatches. Injected actions go through
581/// the full middleware + reducer pipeline. Dispatch limits prevent runaway loops.
582///
583/// Useful for cascading behavior: moving a card to "Done" triggers a notification,
584/// without the move reducer knowing about notifications.
585pub trait Middleware<S, A: Action> {
586    /// Called before the action is dispatched to the reducer.
587    ///
588    /// Return `true` to proceed with dispatch, `false` to cancel.
589    fn before(&mut self, action: &A, state: &S) -> bool;
590
591    /// Called after the action is processed by the reducer.
592    ///
593    /// Return any follow-up actions to dispatch through the full pipeline.
594    fn after(&mut self, action: &A, state_changed: bool, state: &S) -> Vec<A>;
595}
596
597/// A no-op middleware that does nothing
598#[derive(Debug, Clone, Copy, Default)]
599pub struct NoopMiddleware;
600
601impl<S, A: Action> Middleware<S, A> for NoopMiddleware {
602    fn before(&mut self, _action: &A, _state: &S) -> bool {
603        true
604    }
605    fn after(&mut self, _action: &A, _state_changed: bool, _state: &S) -> Vec<A> {
606        vec![]
607    }
608}
609
610/// Middleware that logs actions via the `tracing` crate.
611///
612/// Requires the `tracing` feature.
613#[cfg(feature = "tracing")]
614#[derive(Debug, Clone, Default)]
615pub struct LoggingMiddleware {
616    /// Whether to log before dispatch
617    pub log_before: bool,
618    /// Whether to log after dispatch
619    pub log_after: bool,
620}
621
622#[cfg(feature = "tracing")]
623impl LoggingMiddleware {
624    /// Create a new logging middleware with default settings (log after only)
625    pub fn new() -> Self {
626        Self {
627            log_before: false,
628            log_after: true,
629        }
630    }
631
632    /// Create a logging middleware that logs both before and after
633    pub fn verbose() -> Self {
634        Self {
635            log_before: true,
636            log_after: true,
637        }
638    }
639}
640
641#[cfg(feature = "tracing")]
642impl<S, A: Action> Middleware<S, A> for LoggingMiddleware {
643    fn before(&mut self, action: &A, _state: &S) -> bool {
644        if self.log_before {
645            tracing::debug!(action = %action.name(), "Dispatching action");
646        }
647        true
648    }
649
650    fn after(&mut self, action: &A, state_changed: bool, _state: &S) -> Vec<A> {
651        if self.log_after {
652            tracing::debug!(
653                action = %action.name(),
654                state_changed = state_changed,
655                "Action processed"
656            );
657        }
658        vec![]
659    }
660}
661
662/// Compose multiple middleware into a single middleware
663pub struct ComposedMiddleware<S, A: Action> {
664    middlewares: Vec<Box<dyn Middleware<S, A>>>,
665}
666
667impl<S, A: Action> std::fmt::Debug for ComposedMiddleware<S, A> {
668    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
669        f.debug_struct("ComposedMiddleware")
670            .field("middlewares_count", &self.middlewares.len())
671            .finish()
672    }
673}
674
675impl<S, A: Action> Default for ComposedMiddleware<S, A> {
676    fn default() -> Self {
677        Self::new()
678    }
679}
680
681impl<S, A: Action> ComposedMiddleware<S, A> {
682    /// Create a new composed middleware
683    pub fn new() -> Self {
684        Self {
685            middlewares: Vec::new(),
686        }
687    }
688
689    /// Add a middleware to the composition
690    pub fn add<M: Middleware<S, A> + 'static>(&mut self, middleware: M) {
691        self.middlewares.push(Box::new(middleware));
692    }
693}
694
695impl<S, A: Action> Middleware<S, A> for ComposedMiddleware<S, A> {
696    fn before(&mut self, action: &A, state: &S) -> bool {
697        for middleware in &mut self.middlewares {
698            if !middleware.before(action, state) {
699                return false;
700            }
701        }
702        true
703    }
704
705    fn after(&mut self, action: &A, state_changed: bool, state: &S) -> Vec<A> {
706        let mut injected = Vec::new();
707        // Call in reverse order for proper nesting
708        for middleware in self.middlewares.iter_mut().rev() {
709            injected.extend(middleware.after(action, state_changed, state));
710        }
711        injected
712    }
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718    use crate::ActionCategory;
719
720    #[derive(Default)]
721    struct TestState {
722        counter: i32,
723    }
724
725    #[derive(Clone, Debug)]
726    enum TestAction {
727        Increment,
728        Decrement,
729        NoOp,
730    }
731
732    impl Action for TestAction {
733        fn name(&self) -> &'static str {
734            match self {
735                TestAction::Increment => "Increment",
736                TestAction::Decrement => "Decrement",
737                TestAction::NoOp => "NoOp",
738            }
739        }
740    }
741
742    fn test_reducer(state: &mut TestState, action: TestAction) -> bool {
743        match action {
744            TestAction::Increment => {
745                state.counter += 1;
746                true
747            }
748            TestAction::Decrement => {
749                state.counter -= 1;
750                true
751            }
752            TestAction::NoOp => false,
753        }
754    }
755
756    #[test]
757    fn test_store_dispatch() {
758        let mut store = Store::new(TestState::default(), test_reducer);
759
760        assert!(store.dispatch(TestAction::Increment));
761        assert_eq!(store.state().counter, 1);
762
763        assert!(store.dispatch(TestAction::Increment));
764        assert_eq!(store.state().counter, 2);
765
766        assert!(store.dispatch(TestAction::Decrement));
767        assert_eq!(store.state().counter, 1);
768    }
769
770    #[test]
771    fn test_store_noop() {
772        let mut store = Store::new(TestState::default(), test_reducer);
773
774        assert!(!store.dispatch(TestAction::NoOp));
775        assert_eq!(store.state().counter, 0);
776    }
777
778    #[test]
779    fn test_store_state_mut() {
780        let mut store = Store::new(TestState::default(), test_reducer);
781
782        store.state_mut().counter = 100;
783        assert_eq!(store.state().counter, 100);
784    }
785
786    #[derive(Default)]
787    struct CountingMiddleware {
788        before_count: usize,
789        after_count: usize,
790    }
791
792    impl<S, A: Action> Middleware<S, A> for CountingMiddleware {
793        fn before(&mut self, _action: &A, _state: &S) -> bool {
794            self.before_count += 1;
795            true
796        }
797
798        fn after(&mut self, _action: &A, _state_changed: bool, _state: &S) -> Vec<A> {
799            self.after_count += 1;
800            vec![]
801        }
802    }
803
804    #[test]
805    fn test_store_with_middleware() {
806        let mut store = StoreWithMiddleware::new(
807            TestState::default(),
808            test_reducer,
809            CountingMiddleware::default(),
810        );
811
812        store.dispatch(TestAction::Increment);
813        store.dispatch(TestAction::Increment);
814
815        assert_eq!(store.middleware().before_count, 2);
816        assert_eq!(store.middleware().after_count, 2);
817        assert_eq!(store.state().counter, 2);
818    }
819
820    struct SelfInjectingMiddleware;
821
822    impl Middleware<TestState, TestAction> for SelfInjectingMiddleware {
823        fn before(&mut self, _action: &TestAction, _state: &TestState) -> bool {
824            true
825        }
826
827        fn after(
828            &mut self,
829            action: &TestAction,
830            _state_changed: bool,
831            _state: &TestState,
832        ) -> Vec<TestAction> {
833            vec![action.clone()]
834        }
835    }
836
837    #[test]
838    fn test_try_dispatch_depth_exceeded() {
839        let mut store =
840            StoreWithMiddleware::new(TestState::default(), test_reducer, SelfInjectingMiddleware)
841                .with_dispatch_limits(DispatchLimits {
842                    max_depth: 2,
843                    max_actions: 100,
844                });
845
846        let err = store.try_dispatch(TestAction::Increment).unwrap_err();
847        assert_eq!(
848            err,
849            DispatchError::DepthExceeded {
850                max_depth: 2,
851                action: "Increment",
852            }
853        );
854        assert_eq!(store.state().counter, 2);
855    }
856
857    #[test]
858    fn test_try_dispatch_action_budget_exceeded() {
859        let mut store =
860            StoreWithMiddleware::new(TestState::default(), test_reducer, SelfInjectingMiddleware)
861                .with_dispatch_limits(DispatchLimits {
862                    max_depth: 32,
863                    max_actions: 2,
864                });
865
866        let err = store.try_dispatch(TestAction::Increment).unwrap_err();
867        assert_eq!(
868            err,
869            DispatchError::ActionBudgetExceeded {
870                max_actions: 2,
871                processed: 2,
872                action: "Increment",
873            }
874        );
875        assert_eq!(store.state().counter, 2);
876    }
877
878    struct FiniteCascadeMiddleware {
879        target: i32,
880    }
881
882    impl Middleware<TestState, TestAction> for FiniteCascadeMiddleware {
883        fn before(&mut self, _action: &TestAction, _state: &TestState) -> bool {
884            true
885        }
886
887        fn after(
888            &mut self,
889            action: &TestAction,
890            _state_changed: bool,
891            state: &TestState,
892        ) -> Vec<TestAction> {
893            if matches!(action, TestAction::Increment) && state.counter < self.target {
894                vec![TestAction::Increment]
895            } else {
896                vec![]
897            }
898        }
899    }
900
901    #[test]
902    fn test_try_dispatch_deep_finite_chain_succeeds() {
903        let target = 512usize;
904        let mut store = StoreWithMiddleware::new(
905            TestState::default(),
906            test_reducer,
907            FiniteCascadeMiddleware {
908                target: target as i32,
909            },
910        )
911        .with_dispatch_limits(DispatchLimits {
912            max_depth: target + 1,
913            max_actions: target + 1,
914        });
915
916        let changed = store.try_dispatch(TestAction::Increment).unwrap();
917        assert!(changed);
918        assert_eq!(store.state().counter, target as i32);
919    }
920
921    #[derive(Default)]
922    struct OrderingState {
923        order: Vec<&'static str>,
924    }
925
926    #[derive(Clone, Debug)]
927    enum OrderingAction {
928        Root,
929        Left,
930        Right,
931        Leaf,
932    }
933
934    impl Action for OrderingAction {
935        fn name(&self) -> &'static str {
936            match self {
937                OrderingAction::Root => "Root",
938                OrderingAction::Left => "Left",
939                OrderingAction::Right => "Right",
940                OrderingAction::Leaf => "Leaf",
941            }
942        }
943    }
944
945    fn ordering_reducer(state: &mut OrderingState, action: OrderingAction) -> bool {
946        state.order.push(action.name());
947        true
948    }
949
950    struct OrderingMiddleware;
951
952    impl Middleware<OrderingState, OrderingAction> for OrderingMiddleware {
953        fn before(&mut self, _action: &OrderingAction, _state: &OrderingState) -> bool {
954            true
955        }
956
957        fn after(
958            &mut self,
959            action: &OrderingAction,
960            _state_changed: bool,
961            _state: &OrderingState,
962        ) -> Vec<OrderingAction> {
963            match action {
964                OrderingAction::Root => vec![OrderingAction::Left, OrderingAction::Right],
965                OrderingAction::Left => vec![OrderingAction::Leaf],
966                OrderingAction::Right | OrderingAction::Leaf => vec![],
967            }
968        }
969    }
970
971    #[test]
972    fn test_try_dispatch_injection_order_is_depth_first() {
973        let mut store = StoreWithMiddleware::new(
974            OrderingState::default(),
975            ordering_reducer,
976            OrderingMiddleware,
977        )
978        .with_dispatch_limits(DispatchLimits {
979            max_depth: 8,
980            max_actions: 8,
981        });
982
983        let changed = store.try_dispatch(OrderingAction::Root).unwrap();
984        assert!(changed);
985        assert_eq!(store.state().order, vec!["Root", "Left", "Leaf", "Right"]);
986    }
987
988    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
989    enum ComposeContext {
990        Default,
991        Command,
992    }
993
994    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
995    enum ComposeCategory {
996        Nav,
997        Search,
998        Uncategorized,
999    }
1000
1001    #[derive(Clone, Debug)]
1002    enum ComposeAction {
1003        NavUp,
1004        Search,
1005        Other,
1006    }
1007
1008    impl Action for ComposeAction {
1009        fn name(&self) -> &'static str {
1010            match self {
1011                ComposeAction::NavUp => "NavUp",
1012                ComposeAction::Search => "Search",
1013                ComposeAction::Other => "Other",
1014            }
1015        }
1016    }
1017
1018    impl ActionCategory for ComposeAction {
1019        type Category = ComposeCategory;
1020
1021        fn category(&self) -> Option<&'static str> {
1022            match self {
1023                ComposeAction::NavUp => Some("nav"),
1024                ComposeAction::Search => Some("search"),
1025                ComposeAction::Other => None,
1026            }
1027        }
1028
1029        fn category_enum(&self) -> Self::Category {
1030            match self {
1031                ComposeAction::NavUp => ComposeCategory::Nav,
1032                ComposeAction::Search => ComposeCategory::Search,
1033                ComposeAction::Other => ComposeCategory::Uncategorized,
1034            }
1035        }
1036    }
1037
1038    fn handle_nav(state: &mut usize, _action: ComposeAction) -> &'static str {
1039        *state += 1;
1040        "nav"
1041    }
1042
1043    fn handle_command(state: &mut usize, _action: ComposeAction) -> &'static str {
1044        *state += 10;
1045        "command"
1046    }
1047
1048    fn handle_search(state: &mut usize, _action: ComposeAction) -> &'static str {
1049        *state += 100;
1050        "search"
1051    }
1052
1053    fn handle_default(state: &mut usize, _action: ComposeAction) -> &'static str {
1054        *state += 1000;
1055        "default"
1056    }
1057
1058    fn composed_reducer(
1059        state: &mut usize,
1060        action: ComposeAction,
1061        context: ComposeContext,
1062    ) -> &'static str {
1063        crate::reducer_compose!(state, action, context, {
1064            category "nav" => handle_nav,
1065            context ComposeContext::Command => handle_command,
1066            ComposeAction::Search => handle_search,
1067            _ => handle_default,
1068        })
1069    }
1070
1071    #[test]
1072    fn test_reducer_compose_routes_category() {
1073        let mut state = 0;
1074        let result = composed_reducer(&mut state, ComposeAction::NavUp, ComposeContext::Command);
1075        assert_eq!(result, "nav");
1076        assert_eq!(state, 1);
1077    }
1078
1079    #[test]
1080    fn test_reducer_compose_routes_context() {
1081        let mut state = 0;
1082        let result = composed_reducer(&mut state, ComposeAction::Other, ComposeContext::Command);
1083        assert_eq!(result, "command");
1084        assert_eq!(state, 10);
1085    }
1086
1087    #[test]
1088    fn test_reducer_compose_routes_pattern() {
1089        let mut state = 0;
1090        let result = composed_reducer(&mut state, ComposeAction::Search, ComposeContext::Default);
1091        assert_eq!(result, "search");
1092        assert_eq!(state, 100);
1093    }
1094
1095    #[test]
1096    fn test_reducer_compose_routes_fallback() {
1097        let mut state = 0;
1098        let result = composed_reducer(&mut state, ComposeAction::Other, ComposeContext::Default);
1099        assert_eq!(result, "default");
1100        assert_eq!(state, 1000);
1101    }
1102
1103    // Test 3-argument form (no context)
1104    fn composed_reducer_no_context(state: &mut usize, action: ComposeAction) -> &'static str {
1105        crate::reducer_compose!(state, action, {
1106            category "nav" => handle_nav,
1107            ComposeAction::Search => handle_search,
1108            _ => handle_default,
1109        })
1110    }
1111
1112    #[test]
1113    fn test_reducer_compose_3arg_category() {
1114        let mut state = 0;
1115        let result = composed_reducer_no_context(&mut state, ComposeAction::NavUp);
1116        assert_eq!(result, "nav");
1117        assert_eq!(state, 1);
1118    }
1119
1120    #[test]
1121    fn test_reducer_compose_3arg_pattern() {
1122        let mut state = 0;
1123        let result = composed_reducer_no_context(&mut state, ComposeAction::Search);
1124        assert_eq!(result, "search");
1125        assert_eq!(state, 100);
1126    }
1127
1128    #[test]
1129    fn test_reducer_compose_3arg_fallback() {
1130        let mut state = 0;
1131        let result = composed_reducer_no_context(&mut state, ComposeAction::Other);
1132        assert_eq!(result, "default");
1133        assert_eq!(state, 1000);
1134    }
1135}