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::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
315pub 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_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}