1use crate::Action;
4use std::marker::PhantomData;
5
6pub type Reducer<S, A> = fn(&mut S, A) -> bool;
10
11pub(crate) const DEFAULT_MAX_DISPATCH_DEPTH: usize = 16;
13pub(crate) const DEFAULT_MAX_DISPATCH_ACTIONS: usize = 10_000;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct DispatchLimits {
22 pub max_depth: usize,
24 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#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum DispatchError {
42 DepthExceeded {
44 max_depth: usize,
45 action: &'static str,
46 },
47 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 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#[macro_export]
276macro_rules! reducer_compose {
277 ($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
372pub 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 pub fn new(state: S, reducer: Reducer<S, A>) -> Self {
420 Self {
421 state,
422 reducer,
423 _marker: PhantomData,
424 }
425 }
426
427 pub fn dispatch(&mut self, action: A) -> bool {
432 (self.reducer)(&mut self.state, action)
433 }
434
435 pub fn state(&self) -> &S {
437 &self.state
438 }
439
440 pub fn state_mut(&mut self) -> &mut S {
446 &mut self.state
447 }
448}
449
450pub 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 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 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 pub fn dispatch_limits(&self) -> DispatchLimits {
479 self.dispatch_limits
480 }
481
482 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 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 pub fn state(&self) -> &S {
514 self.store.state()
515 }
516
517 pub fn state_mut(&mut self) -> &mut S {
519 self.store.state_mut()
520 }
521
522 pub fn middleware(&self) -> &M {
524 &self.middleware
525 }
526
527 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
564pub trait Middleware<S, A: Action> {
586 fn before(&mut self, action: &A, state: &S) -> bool;
590
591 fn after(&mut self, action: &A, state_changed: bool, state: &S) -> Vec<A>;
595}
596
597#[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#[cfg(feature = "tracing")]
614#[derive(Debug, Clone, Default)]
615pub struct LoggingMiddleware {
616 pub log_before: bool,
618 pub log_after: bool,
620}
621
622#[cfg(feature = "tracing")]
623impl LoggingMiddleware {
624 pub fn new() -> Self {
626 Self {
627 log_before: false,
628 log_after: true,
629 }
630 }
631
632 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
662pub 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 pub fn new() -> Self {
684 Self {
685 middlewares: Vec::new(),
686 }
687 }
688
689 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 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 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}