1use crate::{
2 KeyMap, StoreField, StoreFieldTrigger,
3 path::{StorePath, StorePathSegment},
4};
5use reactive_graph::{
6 signal::{
7 ArcTrigger,
8 guards::{Mapped, MappedMut, WriteGuard},
9 },
10 traits::{
11 DefinedAt, FlattenOptionRefOption, IsDisposed, Notify, Read,
12 ReadUntracked, Track, UntrackableGuard, Write,
13 },
14};
15use std::{
16 iter,
17 marker::PhantomData,
18 ops::{Deref, DerefMut},
19 panic::Location,
20};
21
22pub struct OptionSubfield<Inner, T> {
29 #[cfg(any(debug_assertions, leptos_debuginfo))]
30 defined_at: &'static Location<'static>,
31 path_segment: StorePathSegment,
32 inner: Inner,
33 ty: PhantomData<T>,
34}
35
36impl<Inner, T> Clone for OptionSubfield<Inner, T>
37where
38 Inner: Clone,
39{
40 fn clone(&self) -> Self {
41 Self {
42 #[cfg(any(debug_assertions, leptos_debuginfo))]
43 defined_at: self.defined_at,
44 path_segment: self.path_segment,
45 inner: self.inner.clone(),
46 ty: self.ty,
47 }
48 }
49}
50
51impl<Inner, T> Copy for OptionSubfield<Inner, T> where Inner: Copy {}
52
53impl<Inner, T> OptionSubfield<Inner, T> {
54 #[track_caller]
56 pub fn new(inner: Inner) -> Self {
57 Self {
58 #[cfg(any(debug_assertions, leptos_debuginfo))]
59 defined_at: Location::caller(),
60 path_segment: 0.into(),
61 inner,
62 ty: PhantomData,
63 }
64 }
65}
66
67impl<Inner, T> StoreField for OptionSubfield<Inner, T>
68where
69 Inner: StoreField<Value = Option<T>>,
70 T: 'static,
71{
72 type Value = T;
73 type Reader = Mapped<Inner::Reader, T>;
74 type Writer = MappedMut<WriteGuard<Vec<ArcTrigger>, Inner::Writer>, T>;
75
76 fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
77 self.inner
78 .path()
79 .into_iter()
80 .chain(iter::once(self.path_segment))
81 }
82
83 fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
84 self.inner
85 .path_unkeyed()
86 .into_iter()
87 .chain(iter::once(self.path_segment))
88 }
89
90 fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
91 self.inner.get_trigger(path)
92 }
93
94 fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
95 self.inner.get_trigger_unkeyed(path)
96 }
97
98 fn reader(&self) -> Option<Self::Reader> {
99 let inner = self.inner.reader()?;
100 if inner.is_none() {
105 return None;
106 }
107 Some(Mapped::new_with_guard(inner, |t| t.as_ref().unwrap()))
108 }
109
110 fn writer(&self) -> Option<Self::Writer> {
111 let mut parent = self.inner.writer()?;
112 if parent.is_none() {
115 return None;
116 }
117 parent.untrack();
120 let triggers = self.triggers_for_current_path();
121 let guard = WriteGuard::new(triggers, parent);
122 Some(MappedMut::new(
123 guard,
124 |t| t.as_ref().unwrap(),
125 |t| t.as_mut().unwrap(),
126 ))
127 }
128
129 #[inline(always)]
130 fn keys(&self) -> Option<KeyMap> {
131 self.inner.keys()
132 }
133
134 #[track_caller]
135 fn track_field(&self) {
136 let mut full_path = self.path().into_iter().collect::<StorePath>();
137 let trigger = self.get_trigger(self.path().into_iter().collect());
138 trigger.this.track();
139 trigger.children.track();
140
141 while !full_path.is_empty() {
142 full_path.pop();
143 let inner = self.get_trigger(full_path.clone());
144 inner.this.track();
145 }
146 }
147}
148
149impl<Inner, T> DefinedAt for OptionSubfield<Inner, T> {
150 fn defined_at(&self) -> Option<&'static Location<'static>> {
151 #[cfg(any(debug_assertions, leptos_debuginfo))]
152 {
153 Some(self.defined_at)
154 }
155 #[cfg(not(any(debug_assertions, leptos_debuginfo)))]
156 {
157 None
158 }
159 }
160}
161
162impl<Inner, T> IsDisposed for OptionSubfield<Inner, T>
163where
164 Inner: IsDisposed,
165{
166 fn is_disposed(&self) -> bool {
167 self.inner.is_disposed()
168 }
169}
170
171impl<Inner, T> Notify for OptionSubfield<Inner, T>
172where
173 Inner: StoreField<Value = Option<T>>,
174 T: 'static,
175{
176 #[track_caller]
177 fn notify(&self) {
178 let trigger = self.get_trigger(self.path().into_iter().collect());
179 trigger.this.notify();
180 trigger.children.notify();
181 }
182}
183
184impl<Inner, T> Track for OptionSubfield<Inner, T>
185where
186 Inner: StoreField<Value = Option<T>> + Track + 'static,
187 T: 'static,
188{
189 #[track_caller]
190 fn track(&self) {
191 self.track_field();
192 }
193}
194
195impl<Inner, T> ReadUntracked for OptionSubfield<Inner, T>
196where
197 Inner: StoreField<Value = Option<T>>,
198 T: 'static,
199{
200 type Value = <Self as StoreField>::Reader;
201
202 fn try_read_untracked(&self) -> Option<Self::Value> {
203 self.reader()
204 }
205}
206
207impl<Inner, T> Write for OptionSubfield<Inner, T>
208where
209 Inner: StoreField<Value = Option<T>>,
210 T: 'static,
211{
212 type Value = T;
213
214 fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
215 self.writer()
216 }
217
218 fn try_write_untracked(
219 &self,
220 ) -> Option<impl DerefMut<Target = Self::Value>> {
221 self.writer().map(|mut writer| {
222 writer.untrack();
223 writer
224 })
225 }
226}
227
228pub trait OptionStoreExt
230where
231 Self: StoreField<Value = Option<Self::Output>>,
232{
233 type Output;
235
236 fn unwrap(self) -> OptionSubfield<Self, Self::Output>;
242
243 fn invert(self) -> Option<OptionSubfield<Self, Self::Output>> {
245 self.map(|f| f)
246 }
247
248 fn map<U>(
255 self,
256 map_fn: impl FnOnce(OptionSubfield<Self, Self::Output>) -> U,
257 ) -> Option<U>;
258
259 fn map_untracked<U>(
265 self,
266 map_fn: impl FnOnce(OptionSubfield<Self, Self::Output>) -> U,
267 ) -> Option<U>;
268}
269
270impl<T, S> OptionStoreExt for S
271where
272 S: StoreField<Value = Option<T>> + Read + ReadUntracked,
273 <S as Read>::Value: Deref<Target = Option<T>>,
274 <S as ReadUntracked>::Value: Deref<Target = Option<T>>,
275{
276 type Output = T;
277
278 fn unwrap(self) -> OptionSubfield<Self, Self::Output> {
279 OptionSubfield::new(self)
280 }
281
282 fn map<U>(
283 self,
284 map_fn: impl FnOnce(OptionSubfield<S, T>) -> U,
285 ) -> Option<U> {
286 if self.try_read().as_deref().flatten().is_some() {
287 Some(map_fn(self.unwrap()))
288 } else {
289 None
290 }
291 }
292
293 fn map_untracked<U>(
294 self,
295 map_fn: impl FnOnce(OptionSubfield<S, T>) -> U,
296 ) -> Option<U> {
297 if self.try_read_untracked().as_deref().flatten().is_some() {
298 Some(map_fn(self.unwrap()))
299 } else {
300 None
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use crate::{self as reactive_stores, Patch as _, Store};
308 use any_spawner::Executor;
309 use reactive_graph::{
310 effect::Effect,
311 traits::{Get, Read, ReadUntracked, Set, Write},
312 };
313 use reactive_stores_macro::Patch;
314 use std::sync::{
315 Arc,
316 atomic::{AtomicUsize, Ordering},
317 };
318
319 pub async fn tick() {
320 Executor::tick().await;
321 }
322
323 #[derive(Debug, Clone, Store)]
324 pub struct User {
325 pub name: Option<Name>,
326 }
327
328 #[derive(Debug, Clone, Store)]
329 pub struct Name {
330 pub first_name: Option<String>,
331 }
332
333 #[tokio::test]
334 async fn substores_reachable_through_option() {
335 use crate::OptionStoreExt;
336
337 _ = any_spawner::Executor::init_tokio();
338
339 let combined_count = Arc::new(AtomicUsize::new(0));
340
341 let store = Store::new(User { name: None });
342
343 Effect::new_sync({
344 let combined_count = Arc::clone(&combined_count);
345 move |prev: Option<()>| {
346 if prev.is_none() {
347 println!("first run");
348 } else {
349 println!("next run");
350 }
351
352 if store.name().read().is_some() {
353 println!(
354 "inner value = {:?}",
355 *store.name().unwrap().first_name().read()
356 );
357 } else {
358 println!("no inner value");
359 }
360
361 combined_count.fetch_add(1, Ordering::Relaxed);
362 }
363 });
364
365 tick().await;
366 store.name().set(Some(Name {
367 first_name: Some("Greg".into()),
368 }));
369 tick().await;
370 store.name().set(None);
371 tick().await;
372 store.name().set(Some(Name {
373 first_name: Some("Bob".into()),
374 }));
375 tick().await;
376 store
377 .name()
378 .unwrap()
379 .first_name()
380 .write()
381 .as_mut()
382 .unwrap()
383 .push_str("!!!");
384 tick().await;
385 assert_eq!(combined_count.load(Ordering::Relaxed), 5);
386 assert_eq!(
387 store
388 .name()
389 .read_untracked()
390 .as_ref()
391 .unwrap()
392 .first_name
393 .as_ref()
394 .unwrap(),
395 "Bob!!!"
396 );
397 }
398
399 #[tokio::test]
400 async fn mapping_over_optional_store_field() {
401 use crate::OptionStoreExt;
402
403 _ = any_spawner::Executor::init_tokio();
404
405 let parent_count = Arc::new(AtomicUsize::new(0));
406 let inner_count = Arc::new(AtomicUsize::new(0));
407
408 let store = Store::new(User { name: None });
409
410 Effect::new_sync({
411 let parent_count = Arc::clone(&parent_count);
412 move |prev: Option<()>| {
413 if prev.is_none() {
414 println!("parent: first run");
415 } else {
416 println!("parent: next run");
417 }
418
419 println!(" is_some = {}", store.name().read().is_some());
420 parent_count.fetch_add(1, Ordering::Relaxed);
421 }
422 });
423 Effect::new_sync({
424 let inner_count = Arc::clone(&inner_count);
425 move |prev: Option<()>| {
426 if prev.is_none() {
427 println!("inner: first run");
428 } else {
429 println!("inner: next run");
430 }
431
432 println!(
433 "store inner value length = {:?}",
434 store.name().map(|inner| inner
435 .first_name()
436 .get()
437 .unwrap_or_default()
438 .len())
439 );
440 inner_count.fetch_add(1, Ordering::Relaxed);
441 }
442 });
443
444 tick().await;
445 assert_eq!(parent_count.load(Ordering::Relaxed), 1);
446 assert_eq!(inner_count.load(Ordering::Relaxed), 1);
447
448 store.name().set(Some(Name {
449 first_name: Some("Greg".into()),
450 }));
451 tick().await;
452 assert_eq!(parent_count.load(Ordering::Relaxed), 2);
453 assert_eq!(inner_count.load(Ordering::Relaxed), 2);
454
455 println!("\nUpdating first name only");
456 store
457 .name()
458 .unwrap()
459 .first_name()
460 .write()
461 .as_mut()
462 .unwrap()
463 .push_str("!!!");
464
465 tick().await;
466 assert_eq!(parent_count.load(Ordering::Relaxed), 3);
467 assert_eq!(inner_count.load(Ordering::Relaxed), 3);
468 }
469
470 #[tokio::test]
471 async fn patch() {
472 use crate::OptionStoreExt;
473
474 _ = any_spawner::Executor::init_tokio();
475
476 #[derive(Debug, Clone, Store, Patch)]
477 struct Outer {
478 inner: Option<Inner>,
479 }
480
481 #[derive(Debug, Clone, Store, Patch)]
482 struct Inner {
483 first: String,
484 second: String,
485 }
486
487 let store = Store::new(Outer {
488 inner: Some(Inner {
489 first: "A".to_owned(),
490 second: "B".to_owned(),
491 }),
492 });
493
494 let parent_count = Arc::new(AtomicUsize::new(0));
495 let inner_first_count = Arc::new(AtomicUsize::new(0));
496 let inner_second_count = Arc::new(AtomicUsize::new(0));
497
498 Effect::new_sync({
499 let parent_count = Arc::clone(&parent_count);
500 move |prev: Option<()>| {
501 if prev.is_none() {
502 println!("parent: first run");
503 } else {
504 println!("parent: next run");
505 }
506
507 println!(" value = {:?}", store.inner().get());
508 parent_count.fetch_add(1, Ordering::Relaxed);
509 }
510 });
511 Effect::new_sync({
512 let inner_first_count = Arc::clone(&inner_first_count);
513 move |prev: Option<()>| {
514 if prev.is_none() {
515 println!("inner_first: first run");
516 } else {
517 println!("inner_first: next run");
518 }
519
520 println!(
530 " value = {:?}",
531 store.inner().map_untracked(|inner| inner.first().get())
532 );
533 inner_first_count.fetch_add(1, Ordering::Relaxed);
534 }
535 });
536 Effect::new_sync({
537 let inner_second_count = Arc::clone(&inner_second_count);
538 move |prev: Option<()>| {
539 if prev.is_none() {
540 println!("inner_second: first run");
541 } else {
542 println!("inner_second: next run");
543 }
544
545 println!(
546 " value = {:?}",
547 store.inner().map(|inner| inner.second().get())
548 );
549 inner_second_count.fetch_add(1, Ordering::Relaxed);
550 }
551 });
552
553 tick().await;
554 assert_eq!(parent_count.load(Ordering::Relaxed), 1);
555 assert_eq!(inner_first_count.load(Ordering::Relaxed), 1);
556 assert_eq!(inner_second_count.load(Ordering::Relaxed), 1);
557
558 println!("\npatching with A/C");
559 store.patch(Outer {
560 inner: Some(Inner {
561 first: "A".to_string(),
562 second: "C".to_string(),
563 }),
564 });
565
566 tick().await;
567 assert_eq!(parent_count.load(Ordering::Relaxed), 2);
568 assert_eq!(inner_first_count.load(Ordering::Relaxed), 1);
569 assert_eq!(inner_second_count.load(Ordering::Relaxed), 2);
570
571 store.patch(Outer { inner: None });
572
573 tick().await;
574 assert_eq!(parent_count.load(Ordering::Relaxed), 3);
575 assert_eq!(inner_first_count.load(Ordering::Relaxed), 2);
576 assert_eq!(inner_second_count.load(Ordering::Relaxed), 3);
577
578 println!("\npatching with A/B");
579 store.patch(Outer {
580 inner: Some(Inner {
581 first: "A".to_string(),
582 second: "B".to_string(),
583 }),
584 });
585
586 tick().await;
587 assert_eq!(parent_count.load(Ordering::Relaxed), 4);
588 assert_eq!(inner_first_count.load(Ordering::Relaxed), 2);
589 assert_eq!(inner_second_count.load(Ordering::Relaxed), 4);
590 }
591
592 #[test]
593 fn unwrap_reads_as_none_after_option_is_cleared() {
594 use crate::OptionStoreExt;
595 use reactive_graph::owner::Owner;
596
597 #[derive(Debug, Clone, Store)]
598 struct State {
599 value: Option<i32>,
600 }
601
602 let owner = Owner::new();
603 owner.set();
604
605 let store = Store::new(State { value: Some(1) });
606
607 let inner = OptionStoreExt::unwrap(store.value());
609 assert_eq!(inner.try_read_untracked().map(|g| *g), Some(1));
610
611 *store.value().write() = None;
613
614 let guard = inner.try_read_untracked();
617 assert!(guard.is_none());
618 assert!(guard.map(|g| *g).is_none());
620
621 assert!(inner.try_write().is_none());
623 }
624}