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),
22}
23
24pub type EffectResult = Result<Box<dyn std::any::Any>, Box<dyn std::error::Error>>;
25pub type EffectFunction = Box<dyn FnOnce() -> EffectResult + Send>;
26
27#[cfg(test)]
28mod tests {
29 use super::*;
30 use crate::{
31 DispatchOp, MiddlewareFn, MiddlewareFnFactory, Reducer, StoreBuilder, StoreError,
32 Subscriber,
33 };
34 use std::sync::{Arc, Mutex};
35 use std::thread;
36 use std::time::Duration;
37
38 #[derive(Debug, Clone, PartialEq)]
40 struct TestState {
41 value: i32,
42 messages: Vec<String>,
43 }
44
45 impl Default for TestState {
46 fn default() -> Self {
47 TestState {
48 value: 0,
49 messages: Vec::new(),
50 }
51 }
52 }
53
54 #[derive(Debug, Clone)]
55 enum TestAction {
56 SetValue(i32),
57 AddValue(i32),
58 AddMessage(String),
59 AsyncTask,
60 ThunkTask(i32),
61 FunctionTask,
62 }
63
64 struct TestReducer;
66
67 impl Reducer<TestState, TestAction> for TestReducer {
68 fn reduce(
69 &self,
70 state: &TestState,
71 action: &TestAction,
72 ) -> crate::DispatchOp<TestState, TestAction> {
73 match action {
74 TestAction::SetValue(value) => {
75 let new_state = TestState {
76 value: *value,
77 messages: state.messages.clone(),
78 };
79 let effect =
81 Effect::Action(TestAction::AddMessage(format!("Set to {}", value)));
82 crate::DispatchOp::Dispatch(new_state, vec![effect])
83 }
84 TestAction::AddValue(value) => {
85 let new_state = TestState {
86 value: state.value + value,
87 messages: state.messages.clone(),
88 };
89 crate::DispatchOp::Dispatch(new_state, vec![])
90 }
91 TestAction::AddMessage(msg) => {
92 let mut new_messages = state.messages.clone();
93 new_messages.push(msg.clone());
94 let new_state = TestState {
95 value: state.value,
96 messages: new_messages,
97 };
98 crate::DispatchOp::Dispatch(new_state, vec![])
99 }
100 TestAction::AsyncTask => {
101 let new_state = TestState {
102 value: state.value,
103 messages: state.messages.clone(),
104 };
105 let effect = Effect::Task(Box::new(|| {
107 thread::sleep(Duration::from_millis(10));
108 }));
109 crate::DispatchOp::Dispatch(new_state, vec![effect])
110 }
111 TestAction::ThunkTask(value) => {
112 let new_state = TestState {
113 value: state.value,
114 messages: state.messages.clone(),
115 };
116 let value_clone = *value; let effect = Effect::Thunk(Box::new(move |dispatcher| {
119 thread::sleep(Duration::from_millis(10));
120 let _ = dispatcher.dispatch(TestAction::AddValue(value_clone));
121 }));
122 crate::DispatchOp::Dispatch(new_state, vec![effect])
123 }
124 TestAction::FunctionTask => {
125 let new_state = TestState {
126 value: state.value,
127 messages: state.messages.clone(),
128 };
129 let key = "test-key".to_string();
132 let effect = Effect::Function(
133 key.clone(),
134 Box::new(move || {
135 thread::sleep(Duration::from_millis(10));
136 Ok(Box::new(format!("Result for {}", key)) as Box<dyn std::any::Any>)
137 }),
138 );
139 crate::DispatchOp::Dispatch(new_state, vec![effect])
140 }
141 }
142 }
143 }
144
145 #[derive(Default)]
147 struct TestSubscriber {
148 states: Arc<Mutex<Vec<TestState>>>,
149 actions: Arc<Mutex<Vec<TestAction>>>,
150 }
151
152 impl Subscriber<TestState, TestAction> for TestSubscriber {
153 fn on_notify(&self, state: &TestState, action: &TestAction) {
154 self.states.lock().unwrap().push(state.clone());
155 self.actions.lock().unwrap().push(action.clone());
156 }
157 }
158
159 impl TestSubscriber {
160 fn get_states(&self) -> Vec<TestState> {
161 self.states.lock().unwrap().clone()
162 }
163
164 fn get_actions(&self) -> Vec<TestAction> {
165 self.actions.lock().unwrap().clone()
166 }
167 }
168
169 #[test]
170 fn test_effect_action() {
171 println!("Testing Effect::Action");
173
174 let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
175 .with_name("test-action-effect".into())
176 .build()
177 .unwrap();
178
179 let subscriber = Arc::new(TestSubscriber::default());
180 store.add_subscriber(subscriber.clone()).unwrap();
181
182 store.dispatch(TestAction::SetValue(42)).unwrap();
184
185 thread::sleep(Duration::from_millis(100));
187
188 store.stop().unwrap();
190
191 let states = subscriber.get_states();
192 let actions = subscriber.get_actions();
193
194 assert!(states.len() >= 1);
196 assert!(actions.len() >= 1);
197
198 assert_eq!(states.last().unwrap().value, 42);
200
201 assert!(actions.iter().any(|a| matches!(a, TestAction::SetValue(42))));
203 assert!(actions.iter().any(|a| matches!(a, TestAction::AddMessage(_))));
204
205 println!("Effect::Action test passed");
206 }
207
208 #[test]
209 fn test_effect_task() {
210 println!("Testing Effect::Task");
212
213 let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
214 .with_name("test-task-effect".into())
215 .build()
216 .unwrap();
217
218 let subscriber = Arc::new(TestSubscriber::default());
219 store.add_subscriber(subscriber.clone()).unwrap();
220
221 store.dispatch(TestAction::AsyncTask).unwrap();
223
224 thread::sleep(Duration::from_millis(100));
226
227 store.stop().unwrap();
229
230 let actions = subscriber.get_actions();
231
232 assert!(actions.iter().any(|a| matches!(a, TestAction::AsyncTask)));
234
235 println!("Effect::Task test passed");
236 }
237
238 #[test]
239 fn test_effect_thunk() {
240 println!("Testing Effect::Thunk");
242
243 let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
244 .with_name("test-thunk-effect".into())
245 .build()
246 .unwrap();
247
248 let subscriber = Arc::new(TestSubscriber::default());
249 store.add_subscriber(subscriber.clone()).unwrap();
250
251 store.dispatch(TestAction::ThunkTask(10)).unwrap();
253
254 thread::sleep(Duration::from_millis(100));
256
257 store.stop().unwrap();
259
260 let states = subscriber.get_states();
261 let actions = subscriber.get_actions();
262
263 assert!(actions.iter().any(|a| matches!(a, TestAction::ThunkTask(10))));
265
266 assert!(actions.iter().any(|a| matches!(a, TestAction::AddValue(10))));
268
269 assert_eq!(states.last().unwrap().value, 10);
271
272 println!("Effect::Thunk test passed");
273 }
274
275 struct TestEffectMiddleware;
276 impl TestEffectMiddleware {
277 fn new() -> Self {
278 Self {}
279 }
280 }
281 impl MiddlewareFnFactory<TestState, TestAction> for TestEffectMiddleware {
282 fn create(
283 &self,
284 inner: MiddlewareFn<TestState, TestAction>,
285 ) -> MiddlewareFn<TestState, TestAction> {
286 Arc::new(move |state: &TestState, action: &TestAction| {
287 let result: Result<DispatchOp<TestState, TestAction>, StoreError> =
289 inner(state, action);
290
291 let (need_to_dispatch, state, effects) = match result {
293 Ok(DispatchOp::Dispatch(state, effects)) => (true, state, effects),
294 Ok(DispatchOp::Keep(state, effects)) => (false, state, effects),
295 Err(e) => {
296 return Err(e);
297 }
298 };
299
300 let new_effects: Vec<Effect<TestAction>> = effects
302 .into_iter()
303 .map(|effect| match effect {
304 Effect::Function(key, function) => {
305 if key == "test-key" {
306 return Effect::Task(Box::new(move || {
308 let result = function();
309 match result {
311 Ok(result) => {
312 let result_string: Box<String> =
313 result.downcast().unwrap();
314 println!("result_string: {:?}", result_string);
315 assert_eq!(
316 result_string.to_string(),
317 "Result for test-key".to_string()
318 );
319 }
320 Err(e) => {
321 assert!(false, "result should be Ok: {:?}", e);
322 }
323 }
324 }));
325 } else {
326 return Effect::Function(key, function);
327 }
328 }
329 Effect::Action(action) => {
330 return Effect::Action(action);
331 }
332 Effect::Task(task) => {
333 return Effect::Task(task);
334 }
335 Effect::Thunk(thunk) => {
336 return Effect::Thunk(thunk);
337 }
338 })
339 .collect();
340
341 if need_to_dispatch {
342 Ok(DispatchOp::Dispatch(state, new_effects))
343 } else {
344 Ok(DispatchOp::Keep(state, new_effects))
345 }
346 })
347 }
348 }
349
350 #[test]
351 fn test_effect_function() {
352 println!("Testing Effect::Function");
354
355 let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
356 .with_name("test-function-effect".into())
357 .with_middleware(Arc::new(TestEffectMiddleware::new()))
358 .build()
359 .unwrap();
360
361 let subscriber = Arc::new(TestSubscriber::default());
362 store.add_subscriber(subscriber.clone()).unwrap();
363
364 store.dispatch(TestAction::FunctionTask).unwrap();
366
367 thread::sleep(Duration::from_millis(100));
369
370 store.stop().unwrap();
372
373 let actions = subscriber.get_actions();
374
375 assert!(actions.iter().any(|a| matches!(a, TestAction::FunctionTask)));
377
378 println!("Effect::Function test passed");
379 }
380
381 #[test]
382 fn test_effect_chain() {
383 println!("Testing Effect chaining");
385
386 let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
387 .with_name("test-effect-chain".into())
388 .build()
389 .unwrap();
390
391 let subscriber = Arc::new(TestSubscriber::default());
392 store.add_subscriber(subscriber.clone()).unwrap();
393
394 store.dispatch(TestAction::SetValue(5)).unwrap();
396 store.dispatch(TestAction::ThunkTask(3)).unwrap();
397 store.dispatch(TestAction::AsyncTask).unwrap();
398
399 thread::sleep(Duration::from_millis(200));
401
402 store.stop().unwrap();
404
405 let actions = subscriber.get_actions();
406
407 assert!(actions.len() >= 3);
409
410 assert!(actions.iter().any(|a| matches!(a, TestAction::SetValue(5))));
412 assert!(actions.iter().any(|a| matches!(a, TestAction::ThunkTask(3))));
413 assert!(actions.iter().any(|a| matches!(a, TestAction::AsyncTask)));
414
415 assert!(actions.iter().any(|a| matches!(a, TestAction::AddValue(3))));
417
418 assert!(actions.iter().any(|a| matches!(a, TestAction::AddMessage(_))));
420
421 println!("Effect chaining test passed");
422 }
423}