Skip to main content

rust_elm/
scope.rs

1use crate::cmd::Cmd;
2use crate::effect::{run_registered_env_task, run_registered_task, Effect, EffectId};
3use crate::optics::{Casepath, CasePath};
4use key_paths_core::RefKpTrait;
5use rust_identified_vec::{Identifiable, IdentifiedVec};
6use crate::reducer::Reducer;
7use std::marker::PhantomData;
8
9/// Focuses nested state/action and runs a child reducer (UDF `Scope`).
10#[derive(Debug)]
11pub struct ScopeReducer<R, PS: 'static, PA: 'static, CS: 'static, CA: 'static, AK: 'static, SK>
12where
13    AK: Clone,
14    SK: RefKpTrait<PS, CS>,
15{
16    pub state_kp: SK,
17    pub action_kp: AK,
18    pub cancel_id: EffectId,
19    pub child: R,
20    _marker: PhantomData<(PA, CA, PS, CS)>,
21}
22
23impl<R, PS: 'static, PA: 'static, CS: 'static, CA: 'static, AK: 'static, SK>
24    ScopeReducer<R, PS, PA, CS, CA, AK, SK>
25where
26    AK: Clone,
27    SK: RefKpTrait<PS, CS>,
28{
29    pub fn new(state_kp: SK, action_kp: AK, cancel_id: EffectId, child: R) -> Self {
30        Self {
31            state_kp,
32            action_kp,
33            cancel_id,
34            child,
35            _marker: PhantomData,
36        }
37    }
38}
39
40impl<R, PS: 'static, PA: 'static, CS: 'static, CA: 'static, AK: 'static, SK> Reducer
41    for ScopeReducer<R, PS, PA, CS, CA, AK, SK>
42where
43    R: Reducer<State = CS, Action = CA>,
44    PA: Send + 'static,
45    CA: Clone + Send + 'static,
46    AK: Casepath<PA, CA> + Clone + Send + Sync + 'static,
47    SK: RefKpTrait<PS, CS>,
48{
49    type State = PS;
50    type Action = PA;
51
52    fn reduce(&self, state: &mut PS, action: PA) -> Cmd<PA> {
53        let Some(child_action) = self.action_kp.extract(&action) else {
54            return Cmd::none();
55        };
56        let Some(child) = self.state_kp.focus_mut(state) else {
57            return Cmd::none();
58        };
59        let cmd = self.child.reduce(child, child_action);
60        lift_cmd(cmd, self.action_kp.clone(), self.cancel_id)
61    }
62}
63
64/// `ifLet` — run child reducer when optional state is `Some`; cancel on dismiss.
65#[derive(Debug)]
66pub struct IfLetReducer<R, PS: 'static, PA: 'static, CS: 'static, CA: 'static, AK: 'static, SK>
67where
68    AK: Clone,
69    SK: RefKpTrait<PS, CS>,
70{
71    pub scope: ScopeReducer<R, PS, PA, CS, CA, AK, SK>,
72    pub dismiss: fn(PA) -> bool,
73    pub clear: fn(&mut PS),
74    _marker: PhantomData<(PA, CA, PS, CS)>,
75}
76
77impl<R, PS: 'static, PA: 'static, CS: 'static, CA: 'static, AK: 'static, SK>
78    IfLetReducer<R, PS, PA, CS, CA, AK, SK>
79where
80    AK: Clone,
81    SK: RefKpTrait<PS, CS>,
82{
83    pub fn new(
84        state_kp: SK,
85        action_kp: AK,
86        dismiss: fn(PA) -> bool,
87        clear: fn(&mut PS),
88        cancel_id: EffectId,
89        child: R,
90    ) -> Self {
91        Self {
92            scope: ScopeReducer::new(state_kp, action_kp, cancel_id, child),
93            dismiss,
94            clear,
95            _marker: PhantomData,
96        }
97    }
98}
99
100impl<R, PS: 'static, PA: 'static, CS: 'static, CA: 'static, AK: 'static, SK> Reducer
101    for IfLetReducer<R, PS, PA, CS, CA, AK, SK>
102where
103    R: Reducer<State = CS, Action = CA>,
104    PA: Clone + Send + 'static,
105    CA: Clone + Send + 'static,
106    AK: Casepath<PA, CA> + Clone + Send + Sync + 'static,
107    SK: RefKpTrait<PS, CS>,
108{
109    type State = PS;
110    type Action = PA;
111
112    fn reduce(&self, state: &mut PS, action: PA) -> Cmd<PA> {
113        if (self.dismiss)(action.clone()) {
114            let had_child = self.scope.state_kp.focus_mut(state).is_some();
115            (self.clear)(state);
116            if had_child {
117                return Cmd::single(Effect::cancel(self.scope.cancel_id));
118            }
119            return Cmd::none();
120        }
121        self.scope.reduce(state, action)
122    }
123}
124
125/// `ifCaseLet` — `ifLet` for enum-variant child state (same mechanics, enum-focused accessors).
126pub type IfCaseLetReducer<R, PS, PA, CS, CA, SK> =
127    IfLetReducer<R, PS, PA, CS, CA, CasePath<'static, PA, CA>, SK>;
128
129/// `optional` — scope that no-ops when child state is absent (no dismiss/cancel).
130pub type OptionalReducer<R, PS, PA, CS, CA, SK> =
131    ScopeReducer<R, PS, PA, CS, CA, CasePath<'static, PA, CA>, SK>;
132
133/// Run a child reducer for each element in an [`IdentifiedVec`].
134#[derive(Clone, Debug)]
135pub struct ForEachReducer<R, PS, PA, CS, CA, Id> {
136    pub get_vec: fn(&mut PS) -> &mut IdentifiedVec<Id, CS>,
137    pub embed: fn(Id, CA) -> PA,
138    pub extract: fn(PA) -> Option<(Id, CA)>,
139    pub extract_remove: fn(PA) -> Option<Id>,
140    pub cancel_id: fn(Id) -> EffectId,
141    pub child: R,
142}
143
144impl<R, PS, PA, CS, CA, Id> ForEachReducer<R, PS, PA, CS, CA, Id> {
145    pub fn new(
146        get_vec: fn(&mut PS) -> &mut IdentifiedVec<Id, CS>,
147        embed: fn(Id, CA) -> PA,
148        extract: fn(PA) -> Option<(Id, CA)>,
149        extract_remove: fn(PA) -> Option<Id>,
150        cancel_id: fn(Id) -> EffectId,
151        child: R,
152    ) -> Self {
153        Self {
154            get_vec,
155            embed,
156            extract,
157            extract_remove,
158            cancel_id,
159            child,
160        }
161    }
162}
163
164impl<R, PS, PA, CS, CA, Id> Reducer for ForEachReducer<R, PS, PA, CS, CA, Id>
165where
166    R: Reducer<State = CS, Action = CA>,
167    CS: Identifiable<Id = Id>,
168    Id: Copy + Eq + std::hash::Hash + Send + Sync + 'static,
169    PA: Clone + Send + 'static,
170    CA: Send + 'static,
171{
172    type State = PS;
173    type Action = PA;
174
175    fn reduce(&self, state: &mut PS, action: PA) -> Cmd<PA> {
176        if let Some(id) = (self.extract_remove)(action.clone()) {
177            let vec = (self.get_vec)(state);
178            if vec.remove(id).is_some() {
179                return Cmd::single(Effect::cancel((self.cancel_id)(id)));
180            }
181            return Cmd::none();
182        }
183
184        let Some((id, child_action)) = (self.extract)(action) else {
185            return Cmd::none();
186        };
187        let cancel_id = (self.cancel_id)(id);
188        let Some(child) = (self.get_vec)(state).get_mut(id) else {
189            return Cmd::none();
190        };
191        let cmd = self.child.reduce(child, child_action);
192        lift_cmd_with_id(cmd, self.embed, id, cancel_id)
193    }
194}
195
196/// Lift child command into parent action space with a per-id embed fn.
197pub fn lift_cmd_with_id<PA, CA, Id>(
198    cmd: Cmd<CA>,
199    embed: fn(Id, CA) -> PA,
200    id: Id,
201    cancel_id: EffectId,
202) -> Cmd<PA>
203where
204    CA: Send + 'static,
205    PA: Send + 'static,
206    Id: Copy + Send + Sync + 'static,
207{
208    match cmd {
209        Cmd::None => Cmd::None,
210        Cmd::Single(effect) => Cmd::Single(tag_cancel_id(
211            map_effect_with_id(effect, embed, id),
212            cancel_id,
213        )),
214        Cmd::Batch(cmds) => Cmd::Batch(
215            cmds.into_iter()
216                .map(|c| lift_cmd_with_id(c, embed, id, cancel_id))
217                .collect(),
218        ),
219    }
220}
221
222fn map_effect_with_id<PA, CA, Id>(
223    effect: Effect<CA>,
224    embed: fn(Id, CA) -> PA,
225    id: Id,
226) -> Effect<PA>
227where
228    CA: Send + 'static,
229    PA: Send + 'static,
230    Id: Copy + Send + Sync + 'static,
231{
232    match effect {
233        Effect::None => Effect::None,
234        Effect::Task { run, .. } => Effect::from_fn(move || {
235            let fut = run();
236            Box::pin(async move { fut.await.map(|a| embed(id, a)) })
237        }),
238        Effect::RegisteredTask { id: task_id } => Effect::from_fn(move || {
239            let fut = run_registered_task::<CA>(task_id);
240            Box::pin(async move { fut.await.map(|a| embed(id, a)) })
241        }),
242        Effect::EnvTask { run, .. } => Effect::from_env_fn(move |env| {
243            let fut = run(env);
244            Box::pin(async move { fut.await.map(|a| embed(id, a)) })
245        }),
246        Effect::RegisteredEnvTask { id: task_id } => Effect::from_env_fn(move |env| {
247            let fut = run_registered_env_task::<CA>(env, task_id);
248            Box::pin(async move { fut.await.map(|a| embed(id, a)) })
249        }),
250        Effect::Batch(items) => Effect::Batch(
251            items
252                .into_iter()
253                .map(|e| map_effect_with_id(e, embed, id))
254                .collect(),
255        ),
256        Effect::Cancellable {
257            id: cid,
258            cancel_in_flight,
259            inner,
260        } => Effect::Cancellable {
261            id: cid,
262            cancel_in_flight,
263            inner: Box::new(map_effect_with_id(*inner, embed, id)),
264        },
265        Effect::Debounce {
266            id: did,
267            duration,
268            inner,
269        } => Effect::Debounce {
270            id: did,
271            duration,
272            inner: Box::new(map_effect_with_id(*inner, embed, id)),
273        },
274        Effect::Throttle {
275            id: tid,
276            duration,
277            latest,
278            inner,
279        } => Effect::Throttle {
280            id: tid,
281            duration,
282            latest,
283            inner: Box::new(map_effect_with_id(*inner, embed, id)),
284        },
285        Effect::RegisteredRun { id: run_id } => Effect::RegisteredRun { id: run_id },
286        Effect::Provide { env, inner } => Effect::Provide {
287            env,
288            inner: Box::new(map_effect_with_id(*inner, embed, id)),
289        },
290        Effect::Retry { attempts, inner } => Effect::Retry {
291            attempts,
292            inner: Box::new(map_effect_with_id(*inner, embed, id)),
293        },
294        Effect::Timeout { duration, inner } => Effect::Timeout {
295            duration,
296            inner: Box::new(map_effect_with_id(*inner, embed, id)),
297        },
298        Effect::Sequence(items) => Effect::Sequence(
299            items
300                .into_iter()
301                .map(|e| map_effect_with_id(e, embed, id))
302                .collect(),
303        ),
304        Effect::Race(items) => Effect::Race(
305            items
306                .into_iter()
307                .map(|e| map_effect_with_id(e, embed, id))
308                .collect(),
309        ),
310        Effect::Catch { inner, recover: _ } => map_effect_with_id(*inner, embed, id),
311        Effect::Cancel { id: cancel_id } => Effect::Cancel { id: cancel_id },
312    }
313}
314
315/// Lift child command into parent action space and tag effects with a cancel id.
316pub fn lift_cmd<PA, CA, CP>(cmd: Cmd<CA>, casepath: CP, cancel_id: EffectId) -> Cmd<PA>
317where
318    CA: Send + 'static,
319    PA: Send + 'static,
320    CP: Casepath<PA, CA> + Clone + Send + Sync + 'static,
321{
322    match cmd {
323        Cmd::None => Cmd::None,
324        Cmd::Single(effect) => Cmd::Single(tag_cancel_id(
325            map_effect_with_casepath(effect, casepath),
326            cancel_id,
327        )),
328        Cmd::Batch(cmds) => Cmd::Batch(
329            cmds.into_iter()
330                .map(|c| lift_cmd(c, casepath.clone(), cancel_id))
331                .collect(),
332        ),
333    }
334}
335
336fn map_effect_with_casepath<PA, CA, CP>(effect: Effect<CA>, casepath: CP) -> Effect<PA>
337where
338    CA: Send + 'static,
339    PA: Send + 'static,
340    CP: Casepath<PA, CA> + Clone + Send + Sync + 'static,
341{
342    match effect {
343        Effect::None => Effect::None,
344        Effect::Task { run, .. } => {
345            let casepath = casepath.clone();
346            Effect::from_fn(move || {
347                let casepath = casepath.clone();
348                let fut = run();
349                Box::pin(async move { fut.await.map(|a| casepath.wrap(a)) })
350            })
351        }
352        Effect::RegisteredTask { id: task_id } => {
353            let casepath = casepath.clone();
354            Effect::from_fn(move || {
355                let casepath = casepath.clone();
356                let fut = run_registered_task::<CA>(task_id);
357                Box::pin(async move { fut.await.map(|a| casepath.wrap(a)) })
358            })
359        }
360        Effect::EnvTask { run, .. } => {
361            let casepath = casepath.clone();
362            Effect::from_env_fn(move |env| {
363                let casepath = casepath.clone();
364                let fut = run(env);
365                Box::pin(async move { fut.await.map(|a| casepath.wrap(a)) })
366            })
367        }
368        Effect::RegisteredEnvTask { id: task_id } => {
369            let casepath = casepath.clone();
370            Effect::from_env_fn(move |env| {
371                let casepath = casepath.clone();
372                let fut = run_registered_env_task::<CA>(env, task_id);
373                Box::pin(async move { fut.await.map(|a| casepath.wrap(a)) })
374            })
375        }
376        Effect::Batch(items) => Effect::Batch(
377            items
378                .into_iter()
379                .map(|e| map_effect_with_casepath(e, casepath.clone()))
380                .collect(),
381        ),
382        Effect::Cancellable {
383            id: cid,
384            cancel_in_flight,
385            inner,
386        } => Effect::Cancellable {
387            id: cid,
388            cancel_in_flight,
389            inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
390        },
391        Effect::Debounce {
392            id: did,
393            duration,
394            inner,
395        } => Effect::Debounce {
396            id: did,
397            duration,
398            inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
399        },
400        Effect::Throttle {
401            id: tid,
402            duration,
403            latest,
404            inner,
405        } => Effect::Throttle {
406            id: tid,
407            duration,
408            latest,
409            inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
410        },
411        Effect::RegisteredRun { id: run_id } => Effect::RegisteredRun { id: run_id },
412        Effect::Provide { env, inner } => Effect::Provide {
413            env,
414            inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
415        },
416        Effect::Retry { attempts, inner } => Effect::Retry {
417            attempts,
418            inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
419        },
420        Effect::Timeout { duration, inner } => Effect::Timeout {
421            duration,
422            inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
423        },
424        Effect::Sequence(items) => Effect::Sequence(
425            items
426                .into_iter()
427                .map(|e| map_effect_with_casepath(e, casepath.clone()))
428                .collect(),
429        ),
430        Effect::Race(items) => Effect::Race(
431            items
432                .into_iter()
433                .map(|e| map_effect_with_casepath(e, casepath.clone()))
434                .collect(),
435        ),
436        Effect::Catch { inner, recover: _ } => map_effect_with_casepath(*inner, casepath),
437        Effect::Cancel { id: cancel_id } => Effect::Cancel { id: cancel_id },
438    }
439}
440
441fn tag_cancel_id<M>(effect: Effect<M>, cancel_id: EffectId) -> Effect<M> {
442    match effect {
443        Effect::None => Effect::None,
444        Effect::Task { run, .. } => Effect::Task { id: cancel_id, run },
445        Effect::RegisteredTask { .. } | Effect::EnvTask { .. } | Effect::RegisteredEnvTask { .. }
446        | Effect::RegisteredRun { .. } => Effect::Cancellable {
447            id: cancel_id,
448            cancel_in_flight: true,
449            inner: Box::new(tag_cancel_id(effect, cancel_id)),
450        },
451        Effect::Batch(items) => {
452            Effect::Batch(items.into_iter().map(|e| tag_cancel_id(e, cancel_id)).collect())
453        }
454        Effect::Cancellable {
455            id,
456            cancel_in_flight,
457            inner,
458        } => Effect::Cancellable {
459            id,
460            cancel_in_flight,
461            inner: Box::new(tag_cancel_id(*inner, cancel_id)),
462        },
463        Effect::Debounce {
464            id,
465            duration,
466            inner,
467        } => Effect::Debounce {
468            id,
469            duration,
470            inner: Box::new(tag_cancel_id(*inner, cancel_id)),
471        },
472        Effect::Throttle {
473            id,
474            duration,
475            latest,
476            inner,
477        } => Effect::Throttle {
478            id,
479            duration,
480            latest,
481            inner: Box::new(tag_cancel_id(*inner, cancel_id)),
482        },
483        Effect::Provide { env, inner } => Effect::Provide {
484            env,
485            inner: Box::new(tag_cancel_id(*inner, cancel_id)),
486        },
487        Effect::Retry { attempts, inner } => Effect::Retry {
488            attempts,
489            inner: Box::new(tag_cancel_id(*inner, cancel_id)),
490        },
491        Effect::Timeout { duration, inner } => Effect::Timeout {
492            duration,
493            inner: Box::new(tag_cancel_id(*inner, cancel_id)),
494        },
495        Effect::Sequence(items) => Effect::Sequence(
496            items
497                .into_iter()
498                .map(|e| tag_cancel_id(e, cancel_id))
499                .collect(),
500        ),
501        Effect::Race(items) => {
502            Effect::Race(items.into_iter().map(|e| tag_cancel_id(e, cancel_id)).collect())
503        }
504        Effect::Catch { inner, recover } => Effect::Catch {
505            inner: Box::new(tag_cancel_id(*inner, cancel_id)),
506            recover,
507        },
508        Effect::Cancel { .. } => effect,
509    }
510}
511
512#[cfg(test)]
513#[allow(dead_code, clippy::type_complexity, clippy::redundant_closure)]
514mod tests {
515    use super::*;
516    use crate::effect::Effect;
517    use crate::optics::CasePath;
518    use key_paths_derive::{Cp, Kp};
519    use rust_identified_vec::IdentifiedVec;
520    use crate::reducer::Reduce;
521
522    #[derive(Default, Debug, PartialEq, Eq, Kp, Cp)]
523    struct Child {
524        n: i32,
525    }
526
527    #[derive(Default, Debug, PartialEq, Eq, Kp)]
528    struct Parent {
529        child: Option<Child>,
530    }
531
532    #[derive(Clone, Debug, PartialEq, Eq, Kp, Cp)]
533    enum ParentAction {
534        Child(ChildAction),
535        Dismiss,
536        Other,
537    }
538
539    #[derive(Clone, Debug, PartialEq, Eq, Kp)]
540    enum ChildAction {
541        Inc(String),
542    }
543
544    fn child_reducer(s: &mut Child, a: ChildAction) -> Cmd<ChildAction> {
545        match a {
546            ChildAction::Inc(_) => s.n += 1,
547        }
548        Cmd::none()
549    }
550
551    // fn child_kp() -> rust_key_paths::KpType<'static, Parent, Child> {
552    //     fn get(p: &Parent) -> Option<&Child> {
553    //         p.child.as_ref()
554    //     }
555    //     fn get_mut(p: &mut Parent) -> Option<&mut Child> {
556    //         p.child.as_mut()
557    //     }
558    //     rust_key_paths::Kp::new(get, get_mut)
559    // }
560
561    fn child_action_kp() -> CasePath<'static, ParentAction, ChildAction> {
562        ParentAction::child_cp()
563    }
564
565    fn dismiss(a: ParentAction) -> bool {
566        matches!(a, ParentAction::Dismiss)
567    }
568
569    fn clear(p: &mut Parent) {
570        p.child = None;
571    }
572
573    #[test]
574    fn scope_accepts_derived_state_kp() {
575        let n_kp = Child::n_cp();
576        assert_eq!(n_kp.get_ref(&Child { n: 42 }), Some(&42));
577
578        let scope = ScopeReducer::new(
579            Parent::child(),
580            ParentAction::child_cp(),
581            1,
582            Reduce::new(child_reducer),
583        );
584        let mut parent = Parent {
585            child: Some(Child { n: 0 }),
586        };
587        scope.reduce(&mut parent, ParentAction::Child(ChildAction::Inc("Akash Soni".to_string())));
588        assert_eq!(parent.child.as_ref().unwrap().n, 1);
589    }
590
591    #[test]
592    fn scope_isolates_child_state() {
593        let scope = ScopeReducer::new(
594           Parent::child(),
595            ParentAction::child_cp(),
596            1,
597            Reduce::new(child_reducer),
598        );
599        let mut parent = Parent {
600            child: Some(Child { n: 0 }),
601        };
602        scope.reduce(&mut parent, ParentAction::Child(ChildAction::Inc("Akash Soni".to_string())));
603        assert_eq!(parent.child.as_ref().unwrap().n, 1);
604        scope.reduce(&mut parent, ParentAction::Other);
605        assert_eq!(parent.child.as_ref().unwrap().n, 1);
606    }
607
608    #[test]
609    fn if_let_dismiss_cancels_and_clears() {
610        let if_let = IfLetReducer::new(
611            Parent::child(),
612            ParentAction::child_cp(),
613            dismiss,
614            clear,
615            42,
616            Reduce::new(child_reducer),
617        );
618        let mut parent = Parent {
619            child: Some(Child { n: 0 }),
620        };
621        let cmd = if_let.reduce(&mut parent, ParentAction::Dismiss);
622        assert!(parent.child.is_none());
623        assert_eq!(cmd.effect_count(), 1);
624        assert!(matches!(
625            cmd.into_effects().first(),
626            Some(Effect::Cancel { id: 42 })
627        ));
628    }
629
630    #[derive(Debug, Clone, PartialEq, Eq)]
631    struct Row {
632        id: u64,
633        n: i32,
634    }
635
636    impl Identifiable for Row {
637        type Id = u64;
638        fn id(&self) -> u64 {
639            self.id
640        }
641    }
642
643    #[derive(Default, Debug, PartialEq, Eq)]
644    struct ListParent {
645        rows: IdentifiedVec<u64, Row>,
646    }
647
648    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
649    enum ListAction {
650        Row(u64, RowAction),
651        Remove(u64),
652    }
653
654    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
655    enum RowAction {
656        Bump,
657    }
658
659    fn row_reducer(r: &mut Row, a: RowAction) -> Cmd<RowAction> {
660        if matches!(a, RowAction::Bump) {
661            r.n += 1;
662        }
663        Cmd::none()
664    }
665
666    fn get_vec(p: &mut ListParent) -> &mut IdentifiedVec<u64, Row> {
667        &mut p.rows
668    }
669
670    fn embed_row(id: u64, a: RowAction) -> ListAction {
671        ListAction::Row(id, a)
672    }
673
674    fn extract_row(a: ListAction) -> Option<(u64, RowAction)> {
675        match a {
676            ListAction::Row(id, action) => Some((id, action)),
677            _ => None,
678        }
679    }
680
681    fn extract_remove(a: ListAction) -> Option<u64> {
682        match a {
683            ListAction::Remove(id) => Some(id),
684            _ => None,
685        }
686    }
687
688    fn cancel_for_id(id: u64) -> EffectId {
689        1000 + id
690    }
691
692    #[test]
693    fn for_each_scopes_by_id() {
694        let for_each = ForEachReducer::new(
695            get_vec,
696            embed_row,
697            extract_row,
698            extract_remove,
699            cancel_for_id,
700            Reduce::new(row_reducer),
701        );
702        let mut parent = ListParent::default();
703        parent.rows.insert(Row { id: 1, n: 0 });
704        parent.rows.insert(Row { id: 2, n: 0 });
705        for_each.reduce(&mut parent, ListAction::Row(1, RowAction::Bump));
706        assert_eq!(parent.rows.get(1).unwrap().n, 1);
707        assert_eq!(parent.rows.get(2).unwrap().n, 0);
708    }
709
710    #[test]
711    fn for_each_remove_emits_cancel() {
712        let for_each = ForEachReducer::new(
713            get_vec,
714            embed_row,
715            extract_row,
716            extract_remove,
717            cancel_for_id,
718            Reduce::new(row_reducer),
719        );
720        let mut parent = ListParent::default();
721        parent.rows.insert(Row { id: 1, n: 0 });
722        let cmd = for_each.reduce(&mut parent, ListAction::Remove(1));
723        assert!(parent.rows.get(1).is_none());
724        assert!(matches!(
725            cmd.into_effects().first(),
726            Some(Effect::Cancel { id: 1001 })
727        ));
728    }
729}