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