1use crate::Action;
4use std::marker::PhantomData;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum NoEffect {}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ReducerResult<E = NoEffect> {
19 pub changed: bool,
21 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 #[inline]
34 pub fn unchanged() -> Self {
35 Self {
36 changed: false,
37 effects: vec![],
38 }
39 }
40
41 #[inline]
43 pub fn changed() -> Self {
44 Self {
45 changed: true,
46 effects: vec![],
47 }
48 }
49
50 #[inline]
52 pub fn effect(effect: E) -> Self {
53 Self {
54 changed: false,
55 effects: vec![effect],
56 }
57 }
58
59 #[inline]
61 pub fn effects(effects: Vec<E>) -> Self {
62 Self {
63 changed: false,
64 effects,
65 }
66 }
67
68 #[inline]
70 pub fn changed_with(effect: E) -> Self {
71 Self {
72 changed: true,
73 effects: vec![effect],
74 }
75 }
76
77 #[inline]
79 pub fn changed_with_many(effects: Vec<E>) -> Self {
80 Self {
81 changed: true,
82 effects,
83 }
84 }
85
86 #[inline]
88 pub fn with(mut self, effect: E) -> Self {
89 self.effects.push(effect);
90 self
91 }
92
93 #[inline]
95 pub fn mark_changed(mut self) -> Self {
96 self.changed = true;
97 self
98 }
99
100 #[inline]
102 pub fn has_effects(&self) -> bool {
103 !self.effects.is_empty()
104 }
105}
106
107pub type Reducer<S, A, E = NoEffect> = fn(&mut S, A) -> ReducerResult<E>;
112
113pub(crate) const DEFAULT_MAX_DISPATCH_DEPTH: usize = 16;
115pub(crate) const DEFAULT_MAX_DISPATCH_ACTIONS: usize = 10_000;
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub struct DispatchLimits {
124 pub max_depth: usize,
126 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#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum DispatchError {
144 DepthExceeded {
146 max_depth: usize,
147 action: &'static str,
148 },
149 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 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#[macro_export]
379macro_rules! reducer_compose {
380 ($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
475pub 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 pub fn new(state: S, reducer: Reducer<S, A, E>) -> Self {
524 Self {
525 state,
526 reducer,
527 _marker: PhantomData,
528 }
529 }
530
531 pub fn dispatch(&mut self, action: A) -> ReducerResult<E> {
536 (self.reducer)(&mut self.state, action)
537 }
538
539 pub fn state(&self) -> &S {
541 &self.state
542 }
543
544 pub fn state_mut(&mut self) -> &mut S {
550 &mut self.state
551 }
552}
553
554pub 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 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 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 pub fn dispatch_limits(&self) -> DispatchLimits {
586 self.dispatch_limits
587 }
588
589 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 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 pub fn state(&self) -> &S {
621 self.store.state()
622 }
623
624 pub fn state_mut(&mut self) -> &mut S {
626 self.store.state_mut()
627 }
628
629 pub fn middleware(&self) -> &M {
631 &self.middleware
632 }
633
634 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
673pub trait Middleware<S, A: Action> {
695 fn before(&mut self, action: &A, state: &S) -> bool;
699
700 fn after(&mut self, action: &A, state_changed: bool, state: &S) -> Vec<A>;
704}
705
706#[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#[cfg(feature = "tracing")]
723#[derive(Debug, Clone, Default)]
724pub struct LoggingMiddleware {
725 pub log_before: bool,
727 pub log_after: bool,
729}
730
731#[cfg(feature = "tracing")]
732impl LoggingMiddleware {
733 pub fn new() -> Self {
735 Self {
736 log_before: false,
737 log_after: true,
738 }
739 }
740
741 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
771pub 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 pub fn new() -> Self {
793 Self {
794 middlewares: Vec::new(),
795 }
796 }
797
798 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 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 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}