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#[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#[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
125pub type IfCaseLetReducer<R, PS, PA, CS, CA, SK> =
127 IfLetReducer<R, PS, PA, CS, CA, CasePath<'static, PA, CA>, SK>;
128
129pub type OptionalReducer<R, PS, PA, CS, CA, SK> =
131 ScopeReducer<R, PS, PA, CS, CA, CasePath<'static, PA, CA>, SK>;
132
133#[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
196pub 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::RetryBackoff {
295 attempts,
296 delay,
297 max_delay,
298 inner,
299 } => Effect::RetryBackoff {
300 attempts,
301 delay,
302 max_delay,
303 inner: Box::new(map_effect_with_id(*inner, embed, id)),
304 },
305 Effect::Timeout { duration, inner } => Effect::Timeout {
306 duration,
307 inner: Box::new(map_effect_with_id(*inner, embed, id)),
308 },
309 Effect::Sequence(items) => Effect::Sequence(
310 items
311 .into_iter()
312 .map(|e| map_effect_with_id(e, embed, id))
313 .collect(),
314 ),
315 Effect::Race(items) => Effect::Race(
316 items
317 .into_iter()
318 .map(|e| map_effect_with_id(e, embed, id))
319 .collect(),
320 ),
321 Effect::Catch { inner, recover: _ } => map_effect_with_id(*inner, embed, id),
322 Effect::Cancel { id: cancel_id } => Effect::Cancel { id: cancel_id },
323 }
324}
325
326pub fn lift_cmd<PA, CA, CP>(cmd: Cmd<CA>, casepath: CP, cancel_id: EffectId) -> Cmd<PA>
328where
329 CA: Send + 'static,
330 PA: Send + 'static,
331 CP: Casepath<PA, CA> + Clone + Send + Sync + 'static,
332{
333 match cmd {
334 Cmd::None => Cmd::None,
335 Cmd::Single(effect) => Cmd::Single(tag_cancel_id(
336 map_effect_with_casepath(effect, casepath),
337 cancel_id,
338 )),
339 Cmd::Batch(cmds) => Cmd::Batch(
340 cmds.into_iter()
341 .map(|c| lift_cmd(c, casepath.clone(), cancel_id))
342 .collect(),
343 ),
344 }
345}
346
347fn map_effect_with_casepath<PA, CA, CP>(effect: Effect<CA>, casepath: CP) -> Effect<PA>
348where
349 CA: Send + 'static,
350 PA: Send + 'static,
351 CP: Casepath<PA, CA> + Clone + Send + Sync + 'static,
352{
353 match effect {
354 Effect::None => Effect::None,
355 Effect::Task { run, .. } => {
356 let casepath = casepath.clone();
357 Effect::from_fn(move || {
358 let casepath = casepath.clone();
359 let fut = run();
360 Box::pin(async move { fut.await.map(|a| casepath.wrap(a)) })
361 })
362 }
363 Effect::RegisteredTask { id: task_id } => {
364 let casepath = casepath.clone();
365 Effect::from_fn(move || {
366 let casepath = casepath.clone();
367 let fut = run_registered_task::<CA>(task_id);
368 Box::pin(async move { fut.await.map(|a| casepath.wrap(a)) })
369 })
370 }
371 Effect::EnvTask { run, .. } => {
372 let casepath = casepath.clone();
373 Effect::from_env_fn(move |env| {
374 let casepath = casepath.clone();
375 let fut = run(env);
376 Box::pin(async move { fut.await.map(|a| casepath.wrap(a)) })
377 })
378 }
379 Effect::RegisteredEnvTask { id: task_id } => {
380 let casepath = casepath.clone();
381 Effect::from_env_fn(move |env| {
382 let casepath = casepath.clone();
383 let fut = run_registered_env_task::<CA>(env, task_id);
384 Box::pin(async move { fut.await.map(|a| casepath.wrap(a)) })
385 })
386 }
387 Effect::Batch(items) => Effect::Batch(
388 items
389 .into_iter()
390 .map(|e| map_effect_with_casepath(e, casepath.clone()))
391 .collect(),
392 ),
393 Effect::Cancellable {
394 id: cid,
395 cancel_in_flight,
396 inner,
397 } => Effect::Cancellable {
398 id: cid,
399 cancel_in_flight,
400 inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
401 },
402 Effect::Debounce {
403 id: did,
404 duration,
405 inner,
406 } => Effect::Debounce {
407 id: did,
408 duration,
409 inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
410 },
411 Effect::Throttle {
412 id: tid,
413 duration,
414 latest,
415 inner,
416 } => Effect::Throttle {
417 id: tid,
418 duration,
419 latest,
420 inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
421 },
422 Effect::RegisteredRun { id: run_id } => Effect::RegisteredRun { id: run_id },
423 Effect::Provide { env, inner } => Effect::Provide {
424 env,
425 inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
426 },
427 Effect::Retry { attempts, inner } => Effect::Retry {
428 attempts,
429 inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
430 },
431 Effect::RetryBackoff {
432 attempts,
433 delay,
434 max_delay,
435 inner,
436 } => Effect::RetryBackoff {
437 attempts,
438 delay,
439 max_delay,
440 inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
441 },
442 Effect::Timeout { duration, inner } => Effect::Timeout {
443 duration,
444 inner: Box::new(map_effect_with_casepath(*inner, casepath.clone())),
445 },
446 Effect::Sequence(items) => Effect::Sequence(
447 items
448 .into_iter()
449 .map(|e| map_effect_with_casepath(e, casepath.clone()))
450 .collect(),
451 ),
452 Effect::Race(items) => Effect::Race(
453 items
454 .into_iter()
455 .map(|e| map_effect_with_casepath(e, casepath.clone()))
456 .collect(),
457 ),
458 Effect::Catch { inner, recover: _ } => map_effect_with_casepath(*inner, casepath),
459 Effect::Cancel { id: cancel_id } => Effect::Cancel { id: cancel_id },
460 }
461}
462
463fn tag_cancel_id<M>(effect: Effect<M>, cancel_id: EffectId) -> Effect<M> {
464 match effect {
465 Effect::None => Effect::None,
466 Effect::Task { run, .. } => Effect::Task { id: cancel_id, run },
467 Effect::RegisteredTask { .. } | Effect::EnvTask { .. } | Effect::RegisteredEnvTask { .. }
468 | Effect::RegisteredRun { .. } => Effect::Cancellable {
469 id: cancel_id,
470 cancel_in_flight: true,
471 inner: Box::new(tag_cancel_id(effect, cancel_id)),
472 },
473 Effect::Batch(items) => {
474 Effect::Batch(items.into_iter().map(|e| tag_cancel_id(e, cancel_id)).collect())
475 }
476 Effect::Cancellable {
477 id,
478 cancel_in_flight,
479 inner,
480 } => Effect::Cancellable {
481 id,
482 cancel_in_flight,
483 inner: Box::new(tag_cancel_id(*inner, cancel_id)),
484 },
485 Effect::Debounce {
486 id,
487 duration,
488 inner,
489 } => Effect::Debounce {
490 id,
491 duration,
492 inner: Box::new(tag_cancel_id(*inner, cancel_id)),
493 },
494 Effect::Throttle {
495 id,
496 duration,
497 latest,
498 inner,
499 } => Effect::Throttle {
500 id,
501 duration,
502 latest,
503 inner: Box::new(tag_cancel_id(*inner, cancel_id)),
504 },
505 Effect::Provide { env, inner } => Effect::Provide {
506 env,
507 inner: Box::new(tag_cancel_id(*inner, cancel_id)),
508 },
509 Effect::Retry { attempts, inner } => Effect::Retry {
510 attempts,
511 inner: Box::new(tag_cancel_id(*inner, cancel_id)),
512 },
513 Effect::RetryBackoff {
514 attempts,
515 delay,
516 max_delay,
517 inner,
518 } => Effect::RetryBackoff {
519 attempts,
520 delay,
521 max_delay,
522 inner: Box::new(tag_cancel_id(*inner, cancel_id)),
523 },
524 Effect::Timeout { duration, inner } => Effect::Timeout {
525 duration,
526 inner: Box::new(tag_cancel_id(*inner, cancel_id)),
527 },
528 Effect::Sequence(items) => Effect::Sequence(
529 items
530 .into_iter()
531 .map(|e| tag_cancel_id(e, cancel_id))
532 .collect(),
533 ),
534 Effect::Race(items) => {
535 Effect::Race(items.into_iter().map(|e| tag_cancel_id(e, cancel_id)).collect())
536 }
537 Effect::Catch { inner, recover } => Effect::Catch {
538 inner: Box::new(tag_cancel_id(*inner, cancel_id)),
539 recover,
540 },
541 Effect::Cancel { .. } => effect,
542 }
543}
544
545#[cfg(test)]
546#[allow(dead_code, clippy::type_complexity, clippy::redundant_closure)]
547mod tests {
548 use super::*;
549 use crate::effect::Effect;
550 use crate::optics::CasePath;
551 use key_paths_derive::{Cp, Kp};
552 use rust_identified_vec::IdentifiedVec;
553 use crate::reducer::Reduce;
554
555 #[derive(Default, Debug, PartialEq, Eq, Kp, Cp)]
556 struct Child {
557 n: i32,
558 }
559
560 #[derive(Default, Debug, PartialEq, Eq, Kp)]
561 struct Parent {
562 child: Option<Child>,
563 }
564
565 #[derive(Clone, Debug, PartialEq, Eq, Kp, Cp)]
566 enum ParentAction {
567 Child(ChildAction),
568 Dismiss,
569 Other,
570 }
571
572 #[derive(Clone, Debug, PartialEq, Eq, Kp)]
573 enum ChildAction {
574 Inc(String),
575 }
576
577 fn child_reducer(s: &mut Child, a: ChildAction) -> Cmd<ChildAction> {
578 match a {
579 ChildAction::Inc(_) => s.n += 1,
580 }
581 Cmd::none()
582 }
583
584 fn child_action_kp() -> CasePath<'static, ParentAction, ChildAction> {
595 ParentAction::child_cp()
596 }
597
598 fn dismiss(a: ParentAction) -> bool {
599 matches!(a, ParentAction::Dismiss)
600 }
601
602 fn clear(p: &mut Parent) {
603 p.child = None;
604 }
605
606 #[test]
607 fn scope_accepts_derived_state_kp() {
608 let n_kp = Child::n_cp();
609 assert_eq!(n_kp.get_ref(&Child { n: 42 }), Some(&42));
610
611 let scope = ScopeReducer::new(
612 Parent::child(),
613 ParentAction::child_cp(),
614 1,
615 Reduce::new(child_reducer),
616 );
617 let mut parent = Parent {
618 child: Some(Child { n: 0 }),
619 };
620 scope.reduce(&mut parent, ParentAction::Child(ChildAction::Inc("Akash Soni".to_string())));
621 assert_eq!(parent.child.as_ref().map(|c| c.n), Some(1));
622 }
623
624 #[test]
625 fn scope_isolates_child_state() {
626 let scope = ScopeReducer::new(
627 Parent::child(),
628 ParentAction::child_cp(),
629 1,
630 Reduce::new(child_reducer),
631 );
632 let mut parent = Parent {
633 child: Some(Child { n: 0 }),
634 };
635 scope.reduce(&mut parent, ParentAction::Child(ChildAction::Inc("Akash Soni".to_string())));
636 assert_eq!(parent.child.as_ref().map(|c| c.n), Some(1));
637 scope.reduce(&mut parent, ParentAction::Other);
638 assert_eq!(parent.child.as_ref().map(|c| c.n), Some(1));
639 }
640
641 #[test]
642 fn if_let_dismiss_cancels_and_clears() {
643 let if_let = IfLetReducer::new(
644 Parent::child(),
645 ParentAction::child_cp(),
646 dismiss,
647 clear,
648 42,
649 Reduce::new(child_reducer),
650 );
651 let mut parent = Parent {
652 child: Some(Child { n: 0 }),
653 };
654 let cmd = if_let.reduce(&mut parent, ParentAction::Dismiss);
655 assert!(parent.child.is_none());
656 assert_eq!(cmd.effect_count(), 1);
657 assert!(matches!(
658 cmd.into_effects().first(),
659 Some(Effect::Cancel { id: 42 })
660 ));
661 }
662
663 #[derive(Debug, Clone, PartialEq, Eq)]
664 struct Row {
665 id: u64,
666 n: i32,
667 }
668
669 impl Identifiable for Row {
670 type Id = u64;
671 fn id(&self) -> u64 {
672 self.id
673 }
674 }
675
676 #[derive(Default, Debug, PartialEq, Eq)]
677 struct ListParent {
678 rows: IdentifiedVec<u64, Row>,
679 }
680
681 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
682 enum ListAction {
683 Row(u64, RowAction),
684 Remove(u64),
685 }
686
687 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
688 enum RowAction {
689 Bump,
690 }
691
692 fn row_reducer(r: &mut Row, a: RowAction) -> Cmd<RowAction> {
693 if matches!(a, RowAction::Bump) {
694 r.n += 1;
695 }
696 Cmd::none()
697 }
698
699 fn get_vec(p: &mut ListParent) -> &mut IdentifiedVec<u64, Row> {
700 &mut p.rows
701 }
702
703 fn embed_row(id: u64, a: RowAction) -> ListAction {
704 ListAction::Row(id, a)
705 }
706
707 fn extract_row(a: ListAction) -> Option<(u64, RowAction)> {
708 match a {
709 ListAction::Row(id, action) => Some((id, action)),
710 _ => None,
711 }
712 }
713
714 fn extract_remove(a: ListAction) -> Option<u64> {
715 match a {
716 ListAction::Remove(id) => Some(id),
717 _ => None,
718 }
719 }
720
721 fn cancel_for_id(id: u64) -> EffectId {
722 1000 + id
723 }
724
725 #[test]
726 fn for_each_scopes_by_id() {
727 let for_each = ForEachReducer::new(
728 get_vec,
729 embed_row,
730 extract_row,
731 extract_remove,
732 cancel_for_id,
733 Reduce::new(row_reducer),
734 );
735 let mut parent = ListParent::default();
736 parent.rows.insert(Row { id: 1, n: 0 });
737 parent.rows.insert(Row { id: 2, n: 0 });
738 for_each.reduce(&mut parent, ListAction::Row(1, RowAction::Bump));
739 assert_eq!(parent.rows.get(1).map(|r| r.n), Some(1));
740 assert_eq!(parent.rows.get(2).map(|r| r.n), Some(0));
741 }
742
743 #[test]
744 fn for_each_remove_emits_cancel() {
745 let for_each = ForEachReducer::new(
746 get_vec,
747 embed_row,
748 extract_row,
749 extract_remove,
750 cancel_for_id,
751 Reduce::new(row_reducer),
752 );
753 let mut parent = ListParent::default();
754 parent.rows.insert(Row { id: 1, n: 0 });
755 let cmd = for_each.reduce(&mut parent, ListAction::Remove(1));
756 assert!(parent.rows.get(1).is_none());
757 assert!(matches!(
758 cmd.into_effects().first(),
759 Some(Effect::Cancel { id: 1001 })
760 ));
761 }
762}