rs_store/
effect.rs

1use crate::Dispatcher;
2
3/// Represents a side effect that can be executed.
4///
5/// `Effect` is used to encapsulate actions that should be performed as a result of a state change.
6/// an effect can be either simple function or more complex thunk that require a dispatcher.
7pub enum Effect<Action> {
8    /// An action that should be dispatched.
9    Action(Action),
10    /// A task which will be executed asynchronously.
11    Task(Box<dyn FnOnce() + Send>),
12    /// A task that takes the dispatcher as an argument.
13    Thunk(Box<dyn FnOnce(Box<dyn Dispatcher<Action>>) + Send>),
14    /// A function which has a result.
15    /// The result is an Any type which can be downcasted to the expected type,
16    /// It is useful when you want to produce an effect without any dependency of 'store'
17    ///
18    /// ### Caution
19    /// The result default ignored, if you want to get the result of the function,
20    /// you can use a middleware like `TestEffectMiddleware`
21    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    // Test state and actions for Effect tests
39    #[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    // Test reducer that produces different types of effects
65    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                    // Effect::Action - dispatch another action
80                    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                    // Effect::Task - simple async task
106                    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                    // Effect::Thunk - task that uses dispatcher
117                    let value_clone = *value; // Clone the value to avoid lifetime issues
118                    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                    // Effect::Function - function that returns a result
130                    // key == test-key
131                    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    // Test subscriber to track state changes
146    #[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        // Test Effect::Action - simple action dispatch
172        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        // Dispatch action that produces Effect::Action
183        store.dispatch(TestAction::SetValue(42)).unwrap();
184
185        // Wait for effect to be processed
186        thread::sleep(Duration::from_millis(100));
187
188        // Stop store to ensure all effects are processed
189        store.stop().unwrap();
190
191        let states = subscriber.get_states();
192        let actions = subscriber.get_actions();
193
194        // Should have received the initial state and the SetValue action
195        assert!(states.len() >= 1);
196        assert!(actions.len() >= 1);
197
198        // The last state should have value 42
199        assert_eq!(states.last().unwrap().value, 42);
200
201        // Should have received both SetValue and AddMessage actions
202        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        // Test Effect::Task - async task execution
211        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        // Dispatch action that produces Effect::Task
222        store.dispatch(TestAction::AsyncTask).unwrap();
223
224        // Wait for effect to be processed
225        thread::sleep(Duration::from_millis(100));
226
227        // Stop store to ensure all effects are processed
228        store.stop().unwrap();
229
230        let actions = subscriber.get_actions();
231
232        // Should have received the AsyncTask action
233        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        // Test Effect::Thunk - task that uses dispatcher
241        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        // Dispatch action that produces Effect::Thunk
252        store.dispatch(TestAction::ThunkTask(10)).unwrap();
253
254        // Wait for effect to be processed
255        thread::sleep(Duration::from_millis(100));
256
257        // Stop store to ensure all effects are processed
258        store.stop().unwrap();
259
260        let states = subscriber.get_states();
261        let actions = subscriber.get_actions();
262
263        // Should have received the ThunkTask action
264        assert!(actions.iter().any(|a| matches!(a, TestAction::ThunkTask(10))));
265
266        // The thunk should have dispatched AddValue action
267        assert!(actions.iter().any(|a| matches!(a, TestAction::AddValue(10))));
268
269        // Final state should have value 10
270        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                // inner
288                let result: Result<DispatchOp<TestState, TestAction>, StoreError> =
289                    inner(state, action);
290
291                // effects 를 순회하면서 function 을 변환
292                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                // convert function effect to task effect if key is "test-key"
301                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                                // convert the function to task or thunk as you want, here we use task
307                                return Effect::Task(Box::new(move || {
308                                    let result = function();
309                                    // result for 'test-key'should be Box<String>
310                                    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        // Test Effect::Function - function that returns a result
353        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        // Dispatch action that produces Effect::Function
365        store.dispatch(TestAction::FunctionTask).unwrap();
366
367        // Wait for effect to be processed
368        thread::sleep(Duration::from_millis(100));
369
370        // Stop store to ensure all effects are processed
371        store.stop().unwrap();
372
373        let actions = subscriber.get_actions();
374
375        // Should have received the FunctionTask action
376        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        // Test chaining multiple effects
384        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        // Dispatch multiple actions with effects
395        store.dispatch(TestAction::SetValue(5)).unwrap();
396        store.dispatch(TestAction::ThunkTask(3)).unwrap();
397        store.dispatch(TestAction::AsyncTask).unwrap();
398
399        // Wait for all effects to be processed
400        thread::sleep(Duration::from_millis(200));
401
402        // Stop store to ensure all effects are processed
403        store.stop().unwrap();
404
405        let actions = subscriber.get_actions();
406
407        // Should have multiple actions
408        assert!(actions.len() >= 3);
409
410        // Should have SetValue, ThunkTask, and AsyncTask
411        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        // Should have AddValue from thunk
416        assert!(actions.iter().any(|a| matches!(a, TestAction::AddValue(3))));
417
418        // Should have AddMessage from SetValue effect
419        assert!(actions.iter().any(|a| matches!(a, TestAction::AddMessage(_))));
420
421        println!("Effect chaining test passed");
422    }
423}