1use crate::Action;
4use std::marker::PhantomData;
5
6pub type Reducer<S, A> = fn(&mut S, A) -> bool;
10
11#[macro_export]
100macro_rules! reducer_compose {
101 ($state:expr, $action:expr, { $($arms:tt)+ }) => {{
103 let __state = $state;
104 let __action_input = $action;
105 let __context = ();
106 $crate::reducer_compose!(@accum __state, __action_input, __context; () $($arms)+)
107 }};
108 ($state:expr, $action:expr, $context:expr, { $($arms:tt)+ }) => {{
109 let __state = $state;
110 let __action_input = $action;
111 let __context = $context;
112 $crate::reducer_compose!(@accum __state, __action_input, __context; () $($arms)+)
113 }};
114 (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) category $category:expr => $handler:expr, $($rest:tt)+) => {
115 $crate::reducer_compose!(
116 @accum $state, $action, $context;
117 (
118 $($out)*
119 __action if $crate::ActionCategory::category(&__action) == Some($category) => {
120 ($handler)($state, __action)
121 },
122 )
123 $($rest)+
124 )
125 };
126 (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) context $context_value:expr => $handler:expr, $($rest:tt)+) => {
127 $crate::reducer_compose!(
128 @accum $state, $action, $context;
129 (
130 $($out)*
131 __action if $context == $context_value => {
132 ($handler)($state, __action)
133 },
134 )
135 $($rest)+
136 )
137 };
138 (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) _ => $handler:expr, $($rest:tt)+) => {
139 $crate::reducer_compose!(
140 @accum $state, $action, $context;
141 (
142 $($out)*
143 __action => {
144 ($handler)($state, __action)
145 },
146 )
147 $($rest)+
148 )
149 };
150 (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) $pattern:pat $(if $guard:expr)? => $handler:expr, $($rest:tt)+) => {
151 $crate::reducer_compose!(
152 @accum $state, $action, $context;
153 (
154 $($out)*
155 __action @ $pattern $(if $guard)? => {
156 ($handler)($state, __action)
157 },
158 )
159 $($rest)+
160 )
161 };
162 (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) category $category:expr => $handler:expr $(,)?) => {
163 match $action {
164 $($out)*
165 __action if $crate::ActionCategory::category(&__action) == Some($category) => {
166 ($handler)($state, __action)
167 }
168 }
169 };
170 (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) context $context_value:expr => $handler:expr $(,)?) => {
171 match $action {
172 $($out)*
173 __action if $context == $context_value => {
174 ($handler)($state, __action)
175 }
176 }
177 };
178 (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) _ => $handler:expr $(,)?) => {
179 match $action {
180 $($out)*
181 __action => {
182 ($handler)($state, __action)
183 }
184 }
185 };
186 (@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) $pattern:pat $(if $guard:expr)? => $handler:expr $(,)?) => {
187 match $action {
188 $($out)*
189 __action @ $pattern $(if $guard)? => {
190 ($handler)($state, __action)
191 }
192 }
193 };
194}
195
196pub struct Store<S, A: Action> {
236 state: S,
237 reducer: Reducer<S, A>,
238 _marker: PhantomData<A>,
239}
240
241impl<S, A: Action> Store<S, A> {
242 pub fn new(state: S, reducer: Reducer<S, A>) -> Self {
244 Self {
245 state,
246 reducer,
247 _marker: PhantomData,
248 }
249 }
250
251 pub fn dispatch(&mut self, action: A) -> bool {
256 (self.reducer)(&mut self.state, action)
257 }
258
259 pub fn state(&self) -> &S {
261 &self.state
262 }
263
264 pub fn state_mut(&mut self) -> &mut S {
270 &mut self.state
271 }
272}
273
274pub struct StoreWithMiddleware<S, A: Action, M: Middleware<S, A>> {
279 store: Store<S, A>,
280 middleware: M,
281 dispatch_depth: usize,
282}
283
284impl<S, A: Action, M: Middleware<S, A>> StoreWithMiddleware<S, A, M> {
285 pub fn new(state: S, reducer: Reducer<S, A>, middleware: M) -> Self {
287 Self {
288 store: Store::new(state, reducer),
289 middleware,
290 dispatch_depth: 0,
291 }
292 }
293
294 pub fn dispatch(&mut self, action: A) -> bool {
300 self.dispatch_depth += 1;
301 assert!(
302 self.dispatch_depth <= MAX_DISPATCH_DEPTH,
303 "middleware dispatch depth exceeded {MAX_DISPATCH_DEPTH} — likely infinite injection loop"
304 );
305
306 if self.middleware.before(&action, &self.store.state) {
307 let mut changed = self.store.dispatch(action.clone());
308 let injected = self.middleware.after(&action, changed, &self.store.state);
309 for a in injected {
310 changed |= self.dispatch(a);
311 }
312 self.dispatch_depth -= 1;
313 changed
314 } else {
315 self.dispatch_depth -= 1;
316 false
317 }
318 }
319
320 pub fn state(&self) -> &S {
322 self.store.state()
323 }
324
325 pub fn state_mut(&mut self) -> &mut S {
327 self.store.state_mut()
328 }
329
330 pub fn middleware(&self) -> &M {
332 &self.middleware
333 }
334
335 pub fn middleware_mut(&mut self) -> &mut M {
337 &mut self.middleware
338 }
339}
340
341pub(crate) const MAX_DISPATCH_DEPTH: usize = 16;
343
344pub trait Middleware<S, A: Action> {
366 fn before(&mut self, action: &A, state: &S) -> bool;
370
371 fn after(&mut self, action: &A, state_changed: bool, state: &S) -> Vec<A>;
375}
376
377#[derive(Debug, Clone, Copy, Default)]
379pub struct NoopMiddleware;
380
381impl<S, A: Action> Middleware<S, A> for NoopMiddleware {
382 fn before(&mut self, _action: &A, _state: &S) -> bool {
383 true
384 }
385 fn after(&mut self, _action: &A, _state_changed: bool, _state: &S) -> Vec<A> {
386 vec![]
387 }
388}
389
390#[cfg(feature = "tracing")]
394#[derive(Debug, Clone, Default)]
395pub struct LoggingMiddleware {
396 pub log_before: bool,
398 pub log_after: bool,
400}
401
402#[cfg(feature = "tracing")]
403impl LoggingMiddleware {
404 pub fn new() -> Self {
406 Self {
407 log_before: false,
408 log_after: true,
409 }
410 }
411
412 pub fn verbose() -> Self {
414 Self {
415 log_before: true,
416 log_after: true,
417 }
418 }
419}
420
421#[cfg(feature = "tracing")]
422impl<S, A: Action> Middleware<S, A> for LoggingMiddleware {
423 fn before(&mut self, action: &A, _state: &S) -> bool {
424 if self.log_before {
425 tracing::debug!(action = %action.name(), "Dispatching action");
426 }
427 true
428 }
429
430 fn after(&mut self, action: &A, state_changed: bool, _state: &S) -> Vec<A> {
431 if self.log_after {
432 tracing::debug!(
433 action = %action.name(),
434 state_changed = state_changed,
435 "Action processed"
436 );
437 }
438 vec![]
439 }
440}
441
442pub struct ComposedMiddleware<S, A: Action> {
444 middlewares: Vec<Box<dyn Middleware<S, A>>>,
445}
446
447impl<S, A: Action> std::fmt::Debug for ComposedMiddleware<S, A> {
448 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
449 f.debug_struct("ComposedMiddleware")
450 .field("middlewares_count", &self.middlewares.len())
451 .finish()
452 }
453}
454
455impl<S, A: Action> Default for ComposedMiddleware<S, A> {
456 fn default() -> Self {
457 Self::new()
458 }
459}
460
461impl<S, A: Action> ComposedMiddleware<S, A> {
462 pub fn new() -> Self {
464 Self {
465 middlewares: Vec::new(),
466 }
467 }
468
469 pub fn add<M: Middleware<S, A> + 'static>(&mut self, middleware: M) {
471 self.middlewares.push(Box::new(middleware));
472 }
473}
474
475impl<S, A: Action> Middleware<S, A> for ComposedMiddleware<S, A> {
476 fn before(&mut self, action: &A, state: &S) -> bool {
477 for middleware in &mut self.middlewares {
478 if !middleware.before(action, state) {
479 return false;
480 }
481 }
482 true
483 }
484
485 fn after(&mut self, action: &A, state_changed: bool, state: &S) -> Vec<A> {
486 let mut injected = Vec::new();
487 for middleware in self.middlewares.iter_mut().rev() {
489 injected.extend(middleware.after(action, state_changed, state));
490 }
491 injected
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::ActionCategory;
499
500 #[derive(Default)]
501 struct TestState {
502 counter: i32,
503 }
504
505 #[derive(Clone, Debug)]
506 enum TestAction {
507 Increment,
508 Decrement,
509 NoOp,
510 }
511
512 impl Action for TestAction {
513 fn name(&self) -> &'static str {
514 match self {
515 TestAction::Increment => "Increment",
516 TestAction::Decrement => "Decrement",
517 TestAction::NoOp => "NoOp",
518 }
519 }
520 }
521
522 fn test_reducer(state: &mut TestState, action: TestAction) -> bool {
523 match action {
524 TestAction::Increment => {
525 state.counter += 1;
526 true
527 }
528 TestAction::Decrement => {
529 state.counter -= 1;
530 true
531 }
532 TestAction::NoOp => false,
533 }
534 }
535
536 #[test]
537 fn test_store_dispatch() {
538 let mut store = Store::new(TestState::default(), test_reducer);
539
540 assert!(store.dispatch(TestAction::Increment));
541 assert_eq!(store.state().counter, 1);
542
543 assert!(store.dispatch(TestAction::Increment));
544 assert_eq!(store.state().counter, 2);
545
546 assert!(store.dispatch(TestAction::Decrement));
547 assert_eq!(store.state().counter, 1);
548 }
549
550 #[test]
551 fn test_store_noop() {
552 let mut store = Store::new(TestState::default(), test_reducer);
553
554 assert!(!store.dispatch(TestAction::NoOp));
555 assert_eq!(store.state().counter, 0);
556 }
557
558 #[test]
559 fn test_store_state_mut() {
560 let mut store = Store::new(TestState::default(), test_reducer);
561
562 store.state_mut().counter = 100;
563 assert_eq!(store.state().counter, 100);
564 }
565
566 #[derive(Default)]
567 struct CountingMiddleware {
568 before_count: usize,
569 after_count: usize,
570 }
571
572 impl<S, A: Action> Middleware<S, A> for CountingMiddleware {
573 fn before(&mut self, _action: &A, _state: &S) -> bool {
574 self.before_count += 1;
575 true
576 }
577
578 fn after(&mut self, _action: &A, _state_changed: bool, _state: &S) -> Vec<A> {
579 self.after_count += 1;
580 vec![]
581 }
582 }
583
584 #[test]
585 fn test_store_with_middleware() {
586 let mut store = StoreWithMiddleware::new(
587 TestState::default(),
588 test_reducer,
589 CountingMiddleware::default(),
590 );
591
592 store.dispatch(TestAction::Increment);
593 store.dispatch(TestAction::Increment);
594
595 assert_eq!(store.middleware().before_count, 2);
596 assert_eq!(store.middleware().after_count, 2);
597 assert_eq!(store.state().counter, 2);
598 }
599
600 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
601 enum ComposeContext {
602 Default,
603 Command,
604 }
605
606 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
607 enum ComposeCategory {
608 Nav,
609 Search,
610 Uncategorized,
611 }
612
613 #[derive(Clone, Debug)]
614 enum ComposeAction {
615 NavUp,
616 Search,
617 Other,
618 }
619
620 impl Action for ComposeAction {
621 fn name(&self) -> &'static str {
622 match self {
623 ComposeAction::NavUp => "NavUp",
624 ComposeAction::Search => "Search",
625 ComposeAction::Other => "Other",
626 }
627 }
628 }
629
630 impl ActionCategory for ComposeAction {
631 type Category = ComposeCategory;
632
633 fn category(&self) -> Option<&'static str> {
634 match self {
635 ComposeAction::NavUp => Some("nav"),
636 ComposeAction::Search => Some("search"),
637 ComposeAction::Other => None,
638 }
639 }
640
641 fn category_enum(&self) -> Self::Category {
642 match self {
643 ComposeAction::NavUp => ComposeCategory::Nav,
644 ComposeAction::Search => ComposeCategory::Search,
645 ComposeAction::Other => ComposeCategory::Uncategorized,
646 }
647 }
648 }
649
650 fn handle_nav(state: &mut usize, _action: ComposeAction) -> &'static str {
651 *state += 1;
652 "nav"
653 }
654
655 fn handle_command(state: &mut usize, _action: ComposeAction) -> &'static str {
656 *state += 10;
657 "command"
658 }
659
660 fn handle_search(state: &mut usize, _action: ComposeAction) -> &'static str {
661 *state += 100;
662 "search"
663 }
664
665 fn handle_default(state: &mut usize, _action: ComposeAction) -> &'static str {
666 *state += 1000;
667 "default"
668 }
669
670 fn composed_reducer(
671 state: &mut usize,
672 action: ComposeAction,
673 context: ComposeContext,
674 ) -> &'static str {
675 crate::reducer_compose!(state, action, context, {
676 category "nav" => handle_nav,
677 context ComposeContext::Command => handle_command,
678 ComposeAction::Search => handle_search,
679 _ => handle_default,
680 })
681 }
682
683 #[test]
684 fn test_reducer_compose_routes_category() {
685 let mut state = 0;
686 let result = composed_reducer(&mut state, ComposeAction::NavUp, ComposeContext::Command);
687 assert_eq!(result, "nav");
688 assert_eq!(state, 1);
689 }
690
691 #[test]
692 fn test_reducer_compose_routes_context() {
693 let mut state = 0;
694 let result = composed_reducer(&mut state, ComposeAction::Other, ComposeContext::Command);
695 assert_eq!(result, "command");
696 assert_eq!(state, 10);
697 }
698
699 #[test]
700 fn test_reducer_compose_routes_pattern() {
701 let mut state = 0;
702 let result = composed_reducer(&mut state, ComposeAction::Search, ComposeContext::Default);
703 assert_eq!(result, "search");
704 assert_eq!(state, 100);
705 }
706
707 #[test]
708 fn test_reducer_compose_routes_fallback() {
709 let mut state = 0;
710 let result = composed_reducer(&mut state, ComposeAction::Other, ComposeContext::Default);
711 assert_eq!(result, "default");
712 assert_eq!(state, 1000);
713 }
714
715 fn composed_reducer_no_context(state: &mut usize, action: ComposeAction) -> &'static str {
717 crate::reducer_compose!(state, action, {
718 category "nav" => handle_nav,
719 ComposeAction::Search => handle_search,
720 _ => handle_default,
721 })
722 }
723
724 #[test]
725 fn test_reducer_compose_3arg_category() {
726 let mut state = 0;
727 let result = composed_reducer_no_context(&mut state, ComposeAction::NavUp);
728 assert_eq!(result, "nav");
729 assert_eq!(state, 1);
730 }
731
732 #[test]
733 fn test_reducer_compose_3arg_pattern() {
734 let mut state = 0;
735 let result = composed_reducer_no_context(&mut state, ComposeAction::Search);
736 assert_eq!(result, "search");
737 assert_eq!(state, 100);
738 }
739
740 #[test]
741 fn test_reducer_compose_3arg_fallback() {
742 let mut state = 0;
743 let result = composed_reducer_no_context(&mut state, ComposeAction::Other);
744 assert_eq!(result, "default");
745 assert_eq!(state, 1000);
746 }
747}