1use crate::Dispatcher;
2
3pub enum Effect<Action> {
8 Action(Action),
10 Task(Box<dyn FnOnce() + Send>),
12 Thunk(Box<dyn FnOnce(Box<dyn Dispatcher<Action>>) + Send>),
14 Function(String, EffectFunction),
19}
20
21pub type EffectResult = Result<Box<dyn std::any::Any>, Box<dyn std::error::Error>>;
22pub type EffectFunction = Box<dyn FnOnce() -> EffectResult + Send>;
23
24pub trait EffectResultReceiver {
26 fn receive(&self, key: String, result: EffectResult);
27}
28
29#[cfg(test)]
30mod tests {
31 use super::*;
32 use crate::{DispatchOp, Reducer, StoreBuilder, Subscriber};
33 use std::sync::{Arc, Mutex};
34 use std::thread;
35 use std::time::Duration;
36
37 #[derive(Debug, Clone, PartialEq)]
39 struct TestState {
40 value: i32,
41 messages: Vec<String>,
42 }
43
44 impl Default for TestState {
45 fn default() -> Self {
46 TestState {
47 value: 0,
48 messages: Vec::new(),
49 }
50 }
51 }
52
53 #[derive(Debug, Clone)]
54 enum TestAction {
55 SetValue(i32),
56 AddValue(i32),
57 AddMessage(String),
58 AsyncTask,
59 ThunkTask(i32),
60 FunctionTask(String),
61 }
62
63 struct TestReducer;
65
66 impl Reducer<TestState, TestAction> for TestReducer {
67 fn reduce(
68 &self,
69 state: &TestState,
70 action: &TestAction,
71 ) -> DispatchOp<TestState, TestAction> {
72 match action {
73 TestAction::SetValue(value) => {
74 let new_state = TestState {
75 value: *value,
76 messages: state.messages.clone(),
77 };
78 let effect =
80 Effect::Action(TestAction::AddMessage(format!("Set to {}", value)));
81 DispatchOp::Dispatch(new_state, Some(effect))
82 }
83 TestAction::AddValue(value) => {
84 let new_state = TestState {
85 value: state.value + value,
86 messages: state.messages.clone(),
87 };
88 DispatchOp::Dispatch(new_state, None)
89 }
90 TestAction::AddMessage(msg) => {
91 let mut new_messages = state.messages.clone();
92 new_messages.push(msg.clone());
93 let new_state = TestState {
94 value: state.value,
95 messages: new_messages,
96 };
97 DispatchOp::Dispatch(new_state, None)
98 }
99 TestAction::AsyncTask => {
100 let new_state = TestState {
101 value: state.value,
102 messages: state.messages.clone(),
103 };
104 let effect = Effect::Task(Box::new(|| {
106 thread::sleep(Duration::from_millis(10));
107 }));
108 DispatchOp::Dispatch(new_state, Some(effect))
109 }
110 TestAction::ThunkTask(value) => {
111 let new_state = TestState {
112 value: state.value,
113 messages: state.messages.clone(),
114 };
115 let value_clone = *value; let effect = Effect::Thunk(Box::new(move |dispatcher| {
118 thread::sleep(Duration::from_millis(10));
119 let _ = dispatcher.dispatch(TestAction::AddValue(value_clone));
120 }));
121 DispatchOp::Dispatch(new_state, Some(effect))
122 }
123 TestAction::FunctionTask(key) => {
124 let new_state = TestState {
125 value: state.value,
126 messages: state.messages.clone(),
127 };
128 let key_clone = key.clone();
130 let effect = Effect::Function(
131 key_clone.clone(),
132 Box::new(move || {
133 thread::sleep(Duration::from_millis(10));
134 Ok(Box::new(format!("Result for {}", key_clone))
135 as Box<dyn std::any::Any>)
136 }),
137 );
138 DispatchOp::Dispatch(new_state, Some(effect))
139 }
140 }
141 }
142 }
143
144 #[derive(Default)]
146 struct TestSubscriber {
147 states: Arc<Mutex<Vec<TestState>>>,
148 actions: Arc<Mutex<Vec<TestAction>>>,
149 }
150
151 impl Subscriber<TestState, TestAction> for TestSubscriber {
152 fn on_notify(&self, state: &TestState, action: &TestAction) {
153 self.states.lock().unwrap().push(state.clone());
154 self.actions.lock().unwrap().push(action.clone());
155 }
156 }
157
158 impl TestSubscriber {
159 fn get_states(&self) -> Vec<TestState> {
160 self.states.lock().unwrap().clone()
161 }
162
163 fn get_actions(&self) -> Vec<TestAction> {
164 self.actions.lock().unwrap().clone()
165 }
166 }
167
168 #[test]
169 fn test_effect_action() {
170 println!("Testing Effect::Action");
172
173 let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
174 .with_name("test-action-effect".into())
175 .build()
176 .unwrap();
177
178 let subscriber = Arc::new(TestSubscriber::default());
179 store.add_subscriber(subscriber.clone()).unwrap();
180
181 store.dispatch(TestAction::SetValue(42)).unwrap();
183
184 thread::sleep(Duration::from_millis(100));
186
187 store.stop().unwrap();
189
190 let states = subscriber.get_states();
191 let actions = subscriber.get_actions();
192
193 assert!(states.len() >= 1);
195 assert!(actions.len() >= 1);
196
197 assert_eq!(states.last().unwrap().value, 42);
199
200 assert!(actions.iter().any(|a| matches!(a, TestAction::SetValue(42))));
202 assert!(actions.iter().any(|a| matches!(a, TestAction::AddMessage(_))));
203
204 println!("Effect::Action test passed");
205 }
206
207 #[test]
208 fn test_effect_task() {
209 println!("Testing Effect::Task");
211
212 let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
213 .with_name("test-task-effect".into())
214 .build()
215 .unwrap();
216
217 let subscriber = Arc::new(TestSubscriber::default());
218 store.add_subscriber(subscriber.clone()).unwrap();
219
220 store.dispatch(TestAction::AsyncTask).unwrap();
222
223 thread::sleep(Duration::from_millis(100));
225
226 store.stop().unwrap();
228
229 let actions = subscriber.get_actions();
230
231 assert!(actions.iter().any(|a| matches!(a, TestAction::AsyncTask)));
233
234 println!("Effect::Task test passed");
235 }
236
237 #[test]
238 fn test_effect_thunk() {
239 println!("Testing Effect::Thunk");
241
242 let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
243 .with_name("test-thunk-effect".into())
244 .build()
245 .unwrap();
246
247 let subscriber = Arc::new(TestSubscriber::default());
248 store.add_subscriber(subscriber.clone()).unwrap();
249
250 store.dispatch(TestAction::ThunkTask(10)).unwrap();
252
253 thread::sleep(Duration::from_millis(100));
255
256 store.stop().unwrap();
258
259 let states = subscriber.get_states();
260 let actions = subscriber.get_actions();
261
262 assert!(actions.iter().any(|a| matches!(a, TestAction::ThunkTask(10))));
264
265 assert!(actions.iter().any(|a| matches!(a, TestAction::AddValue(10))));
267
268 assert_eq!(states.last().unwrap().value, 10);
270
271 println!("Effect::Thunk test passed");
272 }
273
274 #[test]
275 fn test_effect_function() {
276 println!("Testing Effect::Function");
278
279 let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
280 .with_name("test-function-effect".into())
281 .build()
282 .unwrap();
283
284 let subscriber = Arc::new(TestSubscriber::default());
285 store.add_subscriber(subscriber.clone()).unwrap();
286
287 store.dispatch(TestAction::FunctionTask("test-key".to_string())).unwrap();
289
290 thread::sleep(Duration::from_millis(100));
292
293 store.stop().unwrap();
295
296 let actions = subscriber.get_actions();
297
298 assert!(actions.iter().any(|a| matches!(a, TestAction::FunctionTask(_))));
300
301 println!("Effect::Function test passed");
302 }
303
304 #[test]
305 fn test_effect_chain() {
306 println!("Testing Effect chaining");
308
309 let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
310 .with_name("test-effect-chain".into())
311 .build()
312 .unwrap();
313
314 let subscriber = Arc::new(TestSubscriber::default());
315 store.add_subscriber(subscriber.clone()).unwrap();
316
317 store.dispatch(TestAction::SetValue(5)).unwrap();
319 store.dispatch(TestAction::ThunkTask(3)).unwrap();
320 store.dispatch(TestAction::AsyncTask).unwrap();
321
322 thread::sleep(Duration::from_millis(200));
324
325 store.stop().unwrap();
327
328 let actions = subscriber.get_actions();
329
330 assert!(actions.len() >= 3);
332
333 assert!(actions.iter().any(|a| matches!(a, TestAction::SetValue(5))));
335 assert!(actions.iter().any(|a| matches!(a, TestAction::ThunkTask(3))));
336 assert!(actions.iter().any(|a| matches!(a, TestAction::AsyncTask)));
337
338 assert!(actions.iter().any(|a| matches!(a, TestAction::AddValue(3))));
340
341 assert!(actions.iter().any(|a| matches!(a, TestAction::AddMessage(_))));
343
344 println!("Effect chaining test passed");
345 }
346}