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