1use crate::cmd::Cmd;
2use crate::store::{catch_reduce, catch_reduce_panic, ReducePanic};
3
4pub trait Reducer {
6 type State;
7 type Action;
8
9 fn reduce(&self, state: &mut Self::State, action: Self::Action) -> Cmd<Self::Action>;
10}
11
12impl<S, A> Reducer for fn(&mut S, A) -> Cmd<A> {
14 type State = S;
15 type Action = A;
16
17 fn reduce(&self, state: &mut S, action: A) -> Cmd<A> {
18 self(state, action)
19 }
20}
21
22#[inline]
24pub const fn coerce_fn<S, A>(f: fn(&mut S, A) -> Cmd<A>) -> fn(&mut S, A) -> Cmd<A> {
25 f
26}
27
28#[derive(Copy, Clone, Debug, PartialEq, Eq)]
30pub struct Reduce<S, A> {
31 reduce: fn(&mut S, A) -> Cmd<A>,
32}
33
34impl<S, A> Reduce<S, A> {
35 pub const fn new(reduce: fn(&mut S, A) -> Cmd<A>) -> Self {
36 Self { reduce }
37 }
38
39 pub const fn none() -> Self
40 where
41 S: Default,
42 {
43 Self::new(|_, _| Cmd::none())
44 }
45}
46
47impl<S, A> Reducer for Reduce<S, A> {
48 type State = S;
49 type Action = A;
50
51 fn reduce(&self, state: &mut S, action: A) -> Cmd<A> {
52 (self.reduce)(state, action)
53 }
54}
55
56#[derive(Copy, Clone, Debug, PartialEq, Eq)]
58pub struct CombineReducers<R>(pub R);
59
60macro_rules! impl_combine_reducers {
61 ($($R:ident),+) => {
62 impl<S, A, $($R),+> Reducer for CombineReducers<($($R,)+)>
63 where
64 $($R: Reducer<State = S, Action = A>),+,
65 A: Clone,
66 {
67 type State = S;
68 type Action = A;
69
70 fn reduce(&self, state: &mut S, action: A) -> Cmd<A> {
71 let ($($R,)+) = &self.0;
72 Cmd::batch([$( $R.reduce(state, action.clone()) ),+])
73 }
74 }
75 };
76}
77
78impl_combine_reducers!(R1);
79impl_combine_reducers!(R1, R2);
80impl_combine_reducers!(R1, R2, R3);
81impl_combine_reducers!(R1, R2, R3, R4);
82impl_combine_reducers!(R1, R2, R3, R4, R5);
83
84#[derive(Clone, Debug)]
87pub struct CatchReducer<R, F> {
88 inner: R,
89 recover: F,
90}
91
92impl<R, F> CatchReducer<R, F> {
93 pub fn new(inner: R, recover: F) -> Self {
94 Self { inner, recover }
95 }
96}
97
98impl<R, F, S, A> Reducer for CatchReducer<R, F>
99where
100 R: Reducer<State = S, Action = A>,
101 F: Fn(ReducePanic) -> Cmd<A>,
102{
103 type State = S;
104 type Action = A;
105
106 fn reduce(&self, state: &mut S, action: A) -> Cmd<A> {
107 match catch_reduce_panic(state, |s, a| self.inner.reduce(s, a), action) {
108 Ok(cmd) => cmd,
109 Err(panic) => (self.recover)(panic),
110 }
111 }
112}
113
114pub struct RollbackCatchReducer<R, F, S> {
117 inner: R,
118 recover: F,
119 checkpoint: parking_lot::Mutex<S>,
120}
121
122impl<R, F, S> RollbackCatchReducer<R, F, S>
123where
124 S: Clone,
125{
126 pub fn new(inner: R, recover: F, committed: &S) -> Self {
127 Self {
128 inner,
129 recover,
130 checkpoint: parking_lot::Mutex::new(committed.clone()),
131 }
132 }
133}
134
135impl<R, F, S> Clone for RollbackCatchReducer<R, F, S>
136where
137 R: Clone,
138 F: Clone,
139 S: Clone,
140{
141 fn clone(&self) -> Self {
142 Self {
143 inner: self.inner.clone(),
144 recover: self.recover.clone(),
145 checkpoint: parking_lot::Mutex::new(self.checkpoint.lock().clone()),
146 }
147 }
148}
149
150impl<R, F, S> std::fmt::Debug for RollbackCatchReducer<R, F, S>
151where
152 R: std::fmt::Debug,
153 F: std::fmt::Debug,
154{
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 f.debug_struct("RollbackCatchReducer")
157 .field("inner", &self.inner)
158 .field("recover", &self.recover)
159 .finish_non_exhaustive()
160 }
161}
162
163impl<R, F, S, A> Reducer for RollbackCatchReducer<R, F, S>
164where
165 R: Reducer<State = S, Action = A>,
166 S: Clone,
167 F: Fn(ReducePanic) -> Cmd<A>,
168{
169 type State = S;
170 type Action = A;
171
172 fn reduce(&self, state: &mut S, action: A) -> Cmd<A> {
173 let mut checkpoint = self.checkpoint.lock();
174 match catch_reduce(state, &mut *checkpoint, |s, a| self.inner.reduce(s, a), action) {
175 Ok(cmd) => cmd,
176 Err(panic) => (self.recover)(panic),
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::effect::Effect;
185 use crate::panic_on_state_clone;
186 use crate::test_support::allow_state_clones;
187
188 panic_on_state_clone! {
189 #[derive(Default, Debug, PartialEq, Eq)]
190 struct App {
191 a: i32,
192 b: i32,
193 }
194 }
195
196 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
197 enum Action {
198 Tick,
199 }
200
201 fn reduce_a(state: &mut App, _: Action) -> Cmd<Action> {
202 state.a += 1;
203 Cmd::none()
204 }
205
206 fn reduce_b(state: &mut App, _: Action) -> Cmd<Action> {
207 state.b += 10;
208 Cmd::none()
209 }
210
211 fn effect_a(_: &mut App, _: Action) -> Cmd<Action> {
212 Cmd::single(Effect::task(1, || Box::pin(async { Ok(Action::Tick) })))
213 }
214
215 #[test]
216 fn fn_pointer_is_reducer() {
217 let mut app = App::default();
218 let cmd = coerce_fn(reduce_a).reduce(&mut app, Action::Tick);
219 assert_eq!(app.a, 1);
220 assert!(cmd.is_none());
221 }
222
223 #[test]
224 fn reduce_wrapper_delegates() {
225 let r = Reduce::new(reduce_b);
226 let mut app = App::default();
227 r.reduce(&mut app, Action::Tick);
228 assert_eq!(app.b, 10);
229 }
230
231 #[test]
232 fn combine_runs_in_order_and_merges_cmds() {
233 let combined = CombineReducers((coerce_fn(reduce_a), coerce_fn(effect_a)));
234 let mut app = App::default();
235 let cmd = combined.reduce(&mut app, Action::Tick);
236 assert_eq!(app.a, 1);
237 assert_eq!(cmd.effect_count(), 1);
238 }
239
240 #[test]
241 fn combine_three_reducers() {
242 fn inc_a(s: &mut App, _: Action) -> Cmd<Action> {
243 s.a += 1;
244 Cmd::none()
245 }
246 fn inc_b(s: &mut App, _: Action) -> Cmd<Action> {
247 s.b += 1;
248 Cmd::none()
249 }
250 fn noop(_: &mut App, _: Action) -> Cmd<Action> {
251 Cmd::none()
252 }
253
254 let combined = CombineReducers((coerce_fn(inc_a), coerce_fn(inc_b), coerce_fn(noop)));
255 let mut app = App::default();
256 combined.reduce(&mut app, Action::Tick);
257 assert_eq!(app.a, 1);
258 assert_eq!(app.b, 1);
259 }
260
261 #[test]
262 fn reducers_macro_builds_combine() {
263 let combined = crate::reducers![reduce_a, reduce_b];
264 let mut app = App::default();
265 combined.reduce(&mut app, Action::Tick);
266 assert_eq!(app.a, 1);
267 assert_eq!(app.b, 10);
268 }
269
270 #[test]
271 fn catch_reducer_recovers_without_reverting_state() {
272 fn panicking(s: &mut App, _: Action) -> Cmd<Action> {
273 s.a = 99;
274 panic!("boom");
275 }
276
277 fn recover(_: ReducePanic) -> Cmd<Action> {
278 Cmd::none()
279 }
280
281 let mut app = App::default();
282 let caught = CatchReducer::new(coerce_fn(panicking), recover);
283 let cmd = caught.reduce(&mut app, Action::Tick);
284 assert_eq!(app.a, 99);
285 assert!(cmd.is_none());
286 }
287
288 #[test]
289 fn rollback_catch_reducer_unwinds_state_on_panic() {
290 fn panicking(s: &mut App, _: Action) -> Cmd<Action> {
291 s.a = 99;
292 panic!("boom");
293 }
294
295 fn recover(_: ReducePanic) -> Cmd<Action> {
296 Cmd::none()
297 }
298
299 let mut app = App::default();
300 let caught = allow_state_clones(1, || {
301 RollbackCatchReducer::new(coerce_fn(panicking), recover, &app)
302 });
303 let cmd = caught.reduce(&mut app, Action::Tick);
304 assert_eq!(app.a, 0);
305 assert!(cmd.is_none());
306 }
307}