rs_store/
reducer.rs

1use crate::effect::Effect;
2
3/// determine if the action should be dispatched or not
4pub enum DispatchOp<State, Action> {
5    /// Dispatch new state
6    Dispatch(State, Option<Effect<Action>>),
7    /// Keep new state but do not dispatch
8    Keep(State, Option<Effect<Action>>),
9}
10
11/// Reducer reduces the state based on the action.
12pub trait Reducer<State, Action>
13where
14    State: Send + Sync + Clone,
15    Action: Send + Sync + 'static,
16{
17    fn reduce(&self, state: &State, action: &Action) -> DispatchOp<State, Action>;
18}
19
20/// FnReducer is a reducer that is created from a function.
21pub struct FnReducer<F, State, Action>
22where
23    F: Fn(&State, &Action) -> DispatchOp<State, Action>,
24    State: Send + Sync + Clone,
25    Action: Send + Sync + 'static,
26{
27    func: F,
28    _marker: std::marker::PhantomData<(State, Action)>,
29}
30
31impl<F, State, Action> Reducer<State, Action> for FnReducer<F, State, Action>
32where
33    F: Fn(&State, &Action) -> DispatchOp<State, Action>,
34    State: Send + Sync + Clone,
35    Action: Send + Sync + 'static,
36{
37    fn reduce(&self, state: &State, action: &Action) -> DispatchOp<State, Action> {
38        (self.func)(state, action)
39    }
40}
41
42impl<F, State, Action> From<F> for FnReducer<F, State, Action>
43where
44    F: Fn(&State, &Action) -> DispatchOp<State, Action>,
45    State: Send + Sync + Clone,
46    Action: Send + Sync + 'static,
47{
48    fn from(func: F) -> Self {
49        Self {
50            func,
51            _marker: std::marker::PhantomData,
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::subscriber::Subscriber;
60    use crate::StoreBuilder;
61    use std::sync::{Arc, Mutex};
62    use std::thread;
63
64    struct TestSubscriber {
65        state_changes: Arc<Mutex<Vec<i32>>>,
66    }
67
68    impl Subscriber<i32, i32> for TestSubscriber {
69        fn on_notify(&self, state: &i32, _action: &i32) {
70            self.state_changes.lock().unwrap().push(*state);
71        }
72    }
73
74    #[test]
75    fn test_store_continues_after_reducer_panic() {
76        // given
77
78        // A reducer that panics on specific action value
79        struct PanicOnValueReducer {
80            panic_on: i32,
81        }
82
83        impl Reducer<i32, i32> for PanicOnValueReducer {
84            fn reduce(&self, state: &i32, action: &i32) -> DispatchOp<i32, i32> {
85                if *action == self.panic_on {
86                    // Catch the panic and return current state
87                    let result = std::panic::catch_unwind(|| {
88                        panic!("Intentional panic on action {}", action);
89                    });
90                    // keep state if panic
91                    if result.is_err() {
92                        return DispatchOp::Keep(*state, None);
93                    }
94                }
95                // Normal operation for other actions
96                DispatchOp::Dispatch(state + action, None)
97            }
98        }
99
100        // Create store with our test reducer
101        let reducer = Box::new(PanicOnValueReducer { panic_on: 42 });
102        let store = StoreBuilder::new_with_reducer(0, reducer).build().unwrap();
103
104        // Track state changes
105        let state_changes = Arc::new(Mutex::new(Vec::new()));
106        let state_changes_clone = state_changes.clone();
107
108        let subscriber = Arc::new(TestSubscriber {
109            state_changes: state_changes_clone,
110        });
111        store.add_subscriber(subscriber);
112
113        // then
114        // Test sequence of actions
115        store.dispatch(1).unwrap(); // Should work: 0 -> 1
116        store.dispatch(42).unwrap(); // Should panic but be caught: stays at 1
117        store.dispatch(2).unwrap(); // Should work: 1 -> 3
118
119        // Give time for all actions to be processed
120        store.stop();
121
122        // then
123        // Verify final state
124        assert_eq!(store.get_state(), 3);
125        // Verify state change history
126        let changes = state_changes.lock().unwrap();
127        assert_eq!(&*changes, &vec![1, 3]); // Should only have non-panic state changes
128    }
129
130    #[test]
131    fn test_multiple_reducers_continue_after_panic() {
132        // given
133        struct PanicReducer;
134        struct NormalReducer;
135
136        impl Reducer<i32, i32> for PanicReducer {
137            fn reduce(&self, state: &i32, action: &i32) -> DispatchOp<i32, i32> {
138                let result = std::panic::catch_unwind(|| {
139                    panic!("Always panic!");
140                });
141                // keep state if panic
142                if result.is_err() {
143                    return DispatchOp::Keep(*state, None);
144                }
145                DispatchOp::Dispatch(state + action, None)
146            }
147        }
148
149        impl Reducer<i32, i32> for NormalReducer {
150            fn reduce(&self, state: &i32, action: &i32) -> DispatchOp<i32, i32> {
151                DispatchOp::Dispatch(state + action, None)
152            }
153        }
154
155        // Create store with both reducers
156        let store = StoreBuilder::new(0)
157            .with_reducer(Box::new(PanicReducer))
158            .add_reducer(Box::new(NormalReducer))
159            .build()
160            .unwrap();
161
162        // when
163        // Dispatch actions
164        store.dispatch(1).unwrap();
165        store.dispatch(2).unwrap();
166
167        store.stop();
168
169        // then
170        // Even though PanicReducer panics, NormalReducer should still process actions
171        assert_eq!(store.get_state(), 3);
172    }
173
174    #[test]
175    fn test_fn_reducer_basic() {
176        // given
177        let reducer =
178            FnReducer::from(|state: &i32, action: &i32| DispatchOp::Dispatch(state + action, None));
179        let store = StoreBuilder::new_with_reducer(0, Box::new(reducer)).build().unwrap();
180
181        // when
182        store.dispatch(5).unwrap();
183        store.dispatch(3).unwrap();
184        store.stop();
185
186        // then
187        assert_eq!(store.get_state(), 8); // 0 + 5 + 3 = 8
188    }
189
190    #[test]
191    fn test_fn_reducer_with_effect() {
192        // given
193        #[derive(Clone)]
194        enum Action {
195            AddWithEffect(i32),
196            Add(i32),
197        }
198
199        let reducer = FnReducer::from(|state: &i32, action: &Action| {
200            match action {
201                Action::AddWithEffect(i) => {
202                    let new_state = state + i;
203                    let effect = Effect::Action(Action::Add(40)); // Effect that adds 40 more
204                    DispatchOp::Dispatch(new_state, Some(effect))
205                }
206                Action::Add(i) => {
207                    let new_state = state + i;
208                    DispatchOp::Dispatch(new_state, None)
209                }
210            }
211        });
212        let store = StoreBuilder::new_with_reducer(0, Box::new(reducer)).build().unwrap();
213
214        // when
215        store.dispatch(Action::AddWithEffect(2)).unwrap();
216        thread::sleep(std::time::Duration::from_millis(1000)); // Wait for effect to be processed
217        store.stop();
218
219        // then
220        // Initial state(0) + action(2) + effect(40) = 42
221        assert_eq!(store.get_state(), 42);
222    }
223
224    #[test]
225    fn test_fn_reducer_keep_state() {
226        // given
227        let reducer = FnReducer::from(|state: &i32, action: &i32| {
228            if *action < 0 {
229                // Keep current state for negative actions
230                DispatchOp::Keep(*state, None)
231            } else {
232                DispatchOp::Dispatch(state + action, None)
233            }
234        });
235        let store = StoreBuilder::new_with_reducer(0, Box::new(reducer)).build().unwrap();
236
237        // Track state changes
238        let state_changes = Arc::new(Mutex::new(Vec::new()));
239        let state_changes_clone = state_changes.clone();
240
241        let subscriber = Arc::new(TestSubscriber {
242            state_changes: state_changes_clone,
243        });
244        store.add_subscriber(subscriber);
245
246        // when
247        store.dispatch(5).unwrap(); // Should change state
248        store.dispatch(-3).unwrap(); // Should keep state
249        store.dispatch(2).unwrap(); // Should change state
250        store.stop();
251
252        // then
253        assert_eq!(store.get_state(), 7); // 0 + 5 + 2 = 7
254        let changes = state_changes.lock().unwrap();
255        assert_eq!(&*changes, &vec![5, 7]); // Only two state changes should be recorded
256    }
257
258    #[test]
259    fn test_multiple_fn_reducers() {
260        // given
261        let add_reducer =
262            FnReducer::from(|state: &i32, action: &i32| DispatchOp::Dispatch(state + action, None));
263        let double_reducer =
264            FnReducer::from(|state: &i32, _action: &i32| DispatchOp::Dispatch(state * 2, None));
265
266        let store = StoreBuilder::new(0)
267            .with_reducer(Box::new(add_reducer))
268            .add_reducer(Box::new(double_reducer))
269            .build()
270            .unwrap();
271
272        // when
273        store.dispatch(3).unwrap(); // (((0)  +3) *2) = 6
274        store.dispatch(15).unwrap(); // (((6) +15) *2) = 42
275        store.stop();
276
277        // then
278        assert_eq!(store.get_state(), 42);
279    }
280}