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<A>> {
279 store: Store<S, A>,
280 middleware: M,
281}
282
283impl<S, A: Action, M: Middleware<A>> StoreWithMiddleware<S, A, M> {
284 pub fn new(state: S, reducer: Reducer<S, A>, middleware: M) -> Self {
286 Self {
287 store: Store::new(state, reducer),
288 middleware,
289 }
290 }
291
292 pub fn dispatch(&mut self, action: A) -> bool {
294 self.middleware.before(&action);
295 let changed = self.store.dispatch(action.clone());
296 self.middleware.after(&action, changed);
297 changed
298 }
299
300 pub fn state(&self) -> &S {
302 self.store.state()
303 }
304
305 pub fn state_mut(&mut self) -> &mut S {
307 self.store.state_mut()
308 }
309
310 pub fn middleware(&self) -> &M {
312 &self.middleware
313 }
314
315 pub fn middleware_mut(&mut self) -> &mut M {
317 &mut self.middleware
318 }
319}
320
321pub trait Middleware<A: Action> {
326 fn before(&mut self, action: &A);
328
329 fn after(&mut self, action: &A, state_changed: bool);
331}
332
333#[derive(Debug, Clone, Copy, Default)]
335pub struct NoopMiddleware;
336
337impl<A: Action> Middleware<A> for NoopMiddleware {
338 fn before(&mut self, _action: &A) {}
339 fn after(&mut self, _action: &A, _state_changed: bool) {}
340}
341
342#[derive(Debug, Clone, Default)]
344pub struct LoggingMiddleware {
345 pub log_before: bool,
347 pub log_after: bool,
349}
350
351impl LoggingMiddleware {
352 pub fn new() -> Self {
354 Self {
355 log_before: false,
356 log_after: true,
357 }
358 }
359
360 pub fn verbose() -> Self {
362 Self {
363 log_before: true,
364 log_after: true,
365 }
366 }
367}
368
369impl<A: Action> Middleware<A> for LoggingMiddleware {
370 fn before(&mut self, action: &A) {
371 if self.log_before {
372 tracing::debug!(action = %action.name(), "Dispatching action");
373 }
374 }
375
376 fn after(&mut self, action: &A, state_changed: bool) {
377 if self.log_after {
378 tracing::debug!(
379 action = %action.name(),
380 state_changed = state_changed,
381 "Action processed"
382 );
383 }
384 }
385}
386
387pub struct ComposedMiddleware<A: Action> {
389 middlewares: Vec<Box<dyn Middleware<A>>>,
390}
391
392impl<A: Action> std::fmt::Debug for ComposedMiddleware<A> {
393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394 f.debug_struct("ComposedMiddleware")
395 .field("middlewares_count", &self.middlewares.len())
396 .finish()
397 }
398}
399
400impl<A: Action> Default for ComposedMiddleware<A> {
401 fn default() -> Self {
402 Self::new()
403 }
404}
405
406impl<A: Action> ComposedMiddleware<A> {
407 pub fn new() -> Self {
409 Self {
410 middlewares: Vec::new(),
411 }
412 }
413
414 pub fn add<M: Middleware<A> + 'static>(&mut self, middleware: M) {
416 self.middlewares.push(Box::new(middleware));
417 }
418}
419
420impl<A: Action> Middleware<A> for ComposedMiddleware<A> {
421 fn before(&mut self, action: &A) {
422 for middleware in &mut self.middlewares {
423 middleware.before(action);
424 }
425 }
426
427 fn after(&mut self, action: &A, state_changed: bool) {
428 for middleware in self.middlewares.iter_mut().rev() {
430 middleware.after(action, state_changed);
431 }
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::ActionCategory;
439
440 #[derive(Default)]
441 struct TestState {
442 counter: i32,
443 }
444
445 #[derive(Clone, Debug)]
446 enum TestAction {
447 Increment,
448 Decrement,
449 NoOp,
450 }
451
452 impl Action for TestAction {
453 fn name(&self) -> &'static str {
454 match self {
455 TestAction::Increment => "Increment",
456 TestAction::Decrement => "Decrement",
457 TestAction::NoOp => "NoOp",
458 }
459 }
460 }
461
462 fn test_reducer(state: &mut TestState, action: TestAction) -> bool {
463 match action {
464 TestAction::Increment => {
465 state.counter += 1;
466 true
467 }
468 TestAction::Decrement => {
469 state.counter -= 1;
470 true
471 }
472 TestAction::NoOp => false,
473 }
474 }
475
476 #[test]
477 fn test_store_dispatch() {
478 let mut store = Store::new(TestState::default(), test_reducer);
479
480 assert!(store.dispatch(TestAction::Increment));
481 assert_eq!(store.state().counter, 1);
482
483 assert!(store.dispatch(TestAction::Increment));
484 assert_eq!(store.state().counter, 2);
485
486 assert!(store.dispatch(TestAction::Decrement));
487 assert_eq!(store.state().counter, 1);
488 }
489
490 #[test]
491 fn test_store_noop() {
492 let mut store = Store::new(TestState::default(), test_reducer);
493
494 assert!(!store.dispatch(TestAction::NoOp));
495 assert_eq!(store.state().counter, 0);
496 }
497
498 #[test]
499 fn test_store_state_mut() {
500 let mut store = Store::new(TestState::default(), test_reducer);
501
502 store.state_mut().counter = 100;
503 assert_eq!(store.state().counter, 100);
504 }
505
506 #[derive(Default)]
507 struct CountingMiddleware {
508 before_count: usize,
509 after_count: usize,
510 }
511
512 impl<A: Action> Middleware<A> for CountingMiddleware {
513 fn before(&mut self, _action: &A) {
514 self.before_count += 1;
515 }
516
517 fn after(&mut self, _action: &A, _state_changed: bool) {
518 self.after_count += 1;
519 }
520 }
521
522 #[test]
523 fn test_store_with_middleware() {
524 let mut store = StoreWithMiddleware::new(
525 TestState::default(),
526 test_reducer,
527 CountingMiddleware::default(),
528 );
529
530 store.dispatch(TestAction::Increment);
531 store.dispatch(TestAction::Increment);
532
533 assert_eq!(store.middleware().before_count, 2);
534 assert_eq!(store.middleware().after_count, 2);
535 assert_eq!(store.state().counter, 2);
536 }
537
538 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
539 enum ComposeContext {
540 Default,
541 Command,
542 }
543
544 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
545 enum ComposeCategory {
546 Nav,
547 Search,
548 Uncategorized,
549 }
550
551 #[derive(Clone, Debug)]
552 enum ComposeAction {
553 NavUp,
554 Search,
555 Other,
556 }
557
558 impl Action for ComposeAction {
559 fn name(&self) -> &'static str {
560 match self {
561 ComposeAction::NavUp => "NavUp",
562 ComposeAction::Search => "Search",
563 ComposeAction::Other => "Other",
564 }
565 }
566 }
567
568 impl ActionCategory for ComposeAction {
569 type Category = ComposeCategory;
570
571 fn category(&self) -> Option<&'static str> {
572 match self {
573 ComposeAction::NavUp => Some("nav"),
574 ComposeAction::Search => Some("search"),
575 ComposeAction::Other => None,
576 }
577 }
578
579 fn category_enum(&self) -> Self::Category {
580 match self {
581 ComposeAction::NavUp => ComposeCategory::Nav,
582 ComposeAction::Search => ComposeCategory::Search,
583 ComposeAction::Other => ComposeCategory::Uncategorized,
584 }
585 }
586 }
587
588 fn handle_nav(state: &mut usize, _action: ComposeAction) -> &'static str {
589 *state += 1;
590 "nav"
591 }
592
593 fn handle_command(state: &mut usize, _action: ComposeAction) -> &'static str {
594 *state += 10;
595 "command"
596 }
597
598 fn handle_search(state: &mut usize, _action: ComposeAction) -> &'static str {
599 *state += 100;
600 "search"
601 }
602
603 fn handle_default(state: &mut usize, _action: ComposeAction) -> &'static str {
604 *state += 1000;
605 "default"
606 }
607
608 fn composed_reducer(
609 state: &mut usize,
610 action: ComposeAction,
611 context: ComposeContext,
612 ) -> &'static str {
613 crate::reducer_compose!(state, action, context, {
614 category "nav" => handle_nav,
615 context ComposeContext::Command => handle_command,
616 ComposeAction::Search => handle_search,
617 _ => handle_default,
618 })
619 }
620
621 #[test]
622 fn test_reducer_compose_routes_category() {
623 let mut state = 0;
624 let result = composed_reducer(&mut state, ComposeAction::NavUp, ComposeContext::Command);
625 assert_eq!(result, "nav");
626 assert_eq!(state, 1);
627 }
628
629 #[test]
630 fn test_reducer_compose_routes_context() {
631 let mut state = 0;
632 let result = composed_reducer(&mut state, ComposeAction::Other, ComposeContext::Command);
633 assert_eq!(result, "command");
634 assert_eq!(state, 10);
635 }
636
637 #[test]
638 fn test_reducer_compose_routes_pattern() {
639 let mut state = 0;
640 let result = composed_reducer(&mut state, ComposeAction::Search, ComposeContext::Default);
641 assert_eq!(result, "search");
642 assert_eq!(state, 100);
643 }
644
645 #[test]
646 fn test_reducer_compose_routes_fallback() {
647 let mut state = 0;
648 let result = composed_reducer(&mut state, ComposeAction::Other, ComposeContext::Default);
649 assert_eq!(result, "default");
650 assert_eq!(state, 1000);
651 }
652
653 fn composed_reducer_no_context(state: &mut usize, action: ComposeAction) -> &'static str {
655 crate::reducer_compose!(state, action, {
656 category "nav" => handle_nav,
657 ComposeAction::Search => handle_search,
658 _ => handle_default,
659 })
660 }
661
662 #[test]
663 fn test_reducer_compose_3arg_category() {
664 let mut state = 0;
665 let result = composed_reducer_no_context(&mut state, ComposeAction::NavUp);
666 assert_eq!(result, "nav");
667 assert_eq!(state, 1);
668 }
669
670 #[test]
671 fn test_reducer_compose_3arg_pattern() {
672 let mut state = 0;
673 let result = composed_reducer_no_context(&mut state, ComposeAction::Search);
674 assert_eq!(result, "search");
675 assert_eq!(state, 100);
676 }
677
678 #[test]
679 fn test_reducer_compose_3arg_fallback() {
680 let mut state = 0;
681 let result = composed_reducer_no_context(&mut state, ComposeAction::Other);
682 assert_eq!(result, "default");
683 assert_eq!(state, 1000);
684 }
685}