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