1use crate::effect::Effect;
2
3pub enum DispatchOp<State, Action> {
5 Dispatch(State, Option<Effect<Action>>),
7 Keep(State, Option<Effect<Action>>),
9}
10
11pub 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
20pub 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 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 let result = std::panic::catch_unwind(|| {
88 panic!("Intentional panic on action {}", action);
89 });
90 if result.is_err() {
92 return DispatchOp::Keep(*state, None);
93 }
94 }
95 DispatchOp::Dispatch(state + action, None)
97 }
98 }
99
100 let reducer = Box::new(PanicOnValueReducer { panic_on: 42 });
102 let store = StoreBuilder::new_with_reducer(0, reducer).build().unwrap();
103
104 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 store.dispatch(1).unwrap(); store.dispatch(42).unwrap(); store.dispatch(2).unwrap(); store.stop();
121
122 assert_eq!(store.get_state(), 3);
125 let changes = state_changes.lock().unwrap();
127 assert_eq!(&*changes, &vec![1, 3]); }
129
130 #[test]
131 fn test_multiple_reducers_continue_after_panic() {
132 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 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 let store = StoreBuilder::new(0)
157 .with_reducer(Box::new(PanicReducer))
158 .add_reducer(Box::new(NormalReducer))
159 .build()
160 .unwrap();
161
162 store.dispatch(1).unwrap();
165 store.dispatch(2).unwrap();
166
167 store.stop();
168
169 assert_eq!(store.get_state(), 3);
172 }
173
174 #[test]
175 fn test_fn_reducer_basic() {
176 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 store.dispatch(5).unwrap();
183 store.dispatch(3).unwrap();
184 store.stop();
185
186 assert_eq!(store.get_state(), 8); }
189
190 #[test]
191 fn test_fn_reducer_with_effect() {
192 #[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)); 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 store.dispatch(Action::AddWithEffect(2)).unwrap();
216 thread::sleep(std::time::Duration::from_millis(1000)); store.stop();
218
219 assert_eq!(store.get_state(), 42);
222 }
223
224 #[test]
225 fn test_fn_reducer_keep_state() {
226 let reducer = FnReducer::from(|state: &i32, action: &i32| {
228 if *action < 0 {
229 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 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 store.dispatch(5).unwrap(); store.dispatch(-3).unwrap(); store.dispatch(2).unwrap(); store.stop();
251
252 assert_eq!(store.get_state(), 7); let changes = state_changes.lock().unwrap();
255 assert_eq!(&*changes, &vec![5, 7]); }
257
258 #[test]
259 fn test_multiple_fn_reducers() {
260 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 store.dispatch(3).unwrap(); store.dispatch(15).unwrap(); store.stop();
276
277 assert_eq!(store.get_state(), 42);
279 }
280}