mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: initial work on reactive stores
This commit is contained in:
parent
6ca3639c3e
commit
db8f5e4899
16 changed files with 1062 additions and 41 deletions
|
@ -19,6 +19,8 @@ members = [
|
|||
"leptos_macro",
|
||||
"leptos_server",
|
||||
"reactive_graph",
|
||||
"reactive_stores",
|
||||
"reactive_stores_macro",
|
||||
"server_fn",
|
||||
"server_fn_macro",
|
||||
"server_fn/server_fn_macro_default",
|
||||
|
@ -60,6 +62,8 @@ next_tuple = { path = "./next_tuple", version = "0.1.0-alpha" }
|
|||
oco_ref = { path = "./oco", version = "0.2" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-alpha" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-alpha" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-alpha" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-alpha" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-alpha" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-alpha" }
|
||||
|
|
93
reactive_graph/src/computed/selector.rs
Normal file
93
reactive_graph/src/computed/selector.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
use crate::{
|
||||
effect::RenderEffect,
|
||||
signal::ArcRwSignal,
|
||||
traits::{Track, Update},
|
||||
};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{
|
||||
hash::Hash,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
/// A conditional signal that only notifies subscribers when a change
|
||||
/// in the source signal’s value changes whether the given function is true.
|
||||
#[derive(Clone)]
|
||||
pub struct Selector<T>
|
||||
where
|
||||
T: PartialEq + Eq + Clone + Hash + 'static,
|
||||
{
|
||||
subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>>,
|
||||
v: Arc<RwLock<Option<T>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
f: Arc<dyn Fn(&T, &T) -> bool + Send + Sync>,
|
||||
// owning the effect keeps it alive, to keep updating the selector
|
||||
#[allow(dead_code)]
|
||||
effect: Arc<RenderEffect<T>>,
|
||||
}
|
||||
|
||||
impl<T> Selector<T>
|
||||
where
|
||||
T: PartialEq + Eq + Clone + Hash + 'static,
|
||||
{
|
||||
pub fn new(source: impl Fn() -> T + Clone + 'static) -> Self {
|
||||
Self::new_with_fn(source, PartialEq::eq)
|
||||
}
|
||||
|
||||
pub fn new_with_fn(
|
||||
source: impl Fn() -> T + Clone + 'static,
|
||||
f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
|
||||
) -> Self {
|
||||
let subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>> =
|
||||
Default::default();
|
||||
let v: Arc<RwLock<Option<T>>> = Default::default();
|
||||
let f = Arc::new(f) as Arc<dyn Fn(&T, &T) -> bool + Send + Sync>;
|
||||
|
||||
let effect = Arc::new(RenderEffect::new({
|
||||
let subs = Arc::clone(&subs);
|
||||
let f = Arc::clone(&f);
|
||||
let v = Arc::clone(&v);
|
||||
move |prev: Option<T>| {
|
||||
let next_value = source();
|
||||
*v.write().or_poisoned() = Some(next_value.clone());
|
||||
if prev.as_ref() != Some(&next_value) {
|
||||
for (key, signal) in &*subs.read().or_poisoned() {
|
||||
if f(key, &next_value)
|
||||
|| (prev.is_some()
|
||||
&& f(key, prev.as_ref().unwrap()))
|
||||
{
|
||||
signal.update(|n| *n = true);
|
||||
}
|
||||
}
|
||||
}
|
||||
next_value
|
||||
}
|
||||
}));
|
||||
|
||||
Selector { subs, v, f, effect }
|
||||
}
|
||||
|
||||
/// Reactively checks whether the given key is selected.
|
||||
pub fn selected(&self, key: T) -> bool {
|
||||
let read = {
|
||||
let mut subs = self.subs.write().or_poisoned();
|
||||
subs.entry(key.clone())
|
||||
.or_insert_with(|| ArcRwSignal::new(false))
|
||||
.clone()
|
||||
};
|
||||
read.track();
|
||||
(self.f)(&key, self.v.read().or_poisoned().as_ref().unwrap())
|
||||
}
|
||||
|
||||
/// Removes the listener for the given key.
|
||||
pub fn remove(&self, key: &T) {
|
||||
let mut subs = self.subs.write().or_poisoned();
|
||||
subs.remove(key);
|
||||
}
|
||||
|
||||
/// Clears the listeners for all keys.
|
||||
pub fn clear(&self) {
|
||||
let mut subs = self.subs.write().or_poisoned();
|
||||
subs.clear();
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use super::{
|
|||
use crate::{
|
||||
graph::{ReactiveNode, SubscriberSet},
|
||||
prelude::{IsDisposed, Trigger},
|
||||
traits::{DefinedAt, ReadUntracked, Writeable},
|
||||
traits::{DefinedAt, ReadUntracked, UntrackableGuard, Writeable},
|
||||
};
|
||||
use core::fmt::{Debug, Formatter, Result};
|
||||
use std::{
|
||||
|
@ -156,13 +156,11 @@ impl<T> Trigger for ArcRwSignal<T> {
|
|||
impl<T: 'static> Writeable for ArcRwSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
fn try_write(
|
||||
&self,
|
||||
) -> Option<WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>>> {
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
self.value
|
||||
.write()
|
||||
.ok()
|
||||
.map(|guard| WriteGuard::new(self, guard))
|
||||
.map(|guard| WriteGuard::new(self.clone(), guard))
|
||||
}
|
||||
|
||||
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>> {
|
||||
|
|
|
@ -107,11 +107,11 @@ impl<T: 'static> Writeable for ArcWriteSignal<T> {
|
|||
|
||||
fn try_write(
|
||||
&self,
|
||||
) -> Option<WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>>> {
|
||||
) -> Option<WriteGuard<Self, impl DerefMut<Target = Self::Value>>> {
|
||||
self.value
|
||||
.write()
|
||||
.ok()
|
||||
.map(|guard| WriteGuard::new(self, guard))
|
||||
.map(|guard| WriteGuard::new(self.clone(), guard))
|
||||
}
|
||||
|
||||
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>> {
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use crate::{computed::BlockingLock, traits::Trigger};
|
||||
use crate::{
|
||||
computed::BlockingLock,
|
||||
traits::{Trigger, UntrackableGuard},
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
use guardian::{ArcRwLockReadGuardian, ArcRwLockWriteGuardian};
|
||||
use std::{
|
||||
|
@ -87,7 +90,7 @@ impl<T: 'static> Debug for Plain<T> {
|
|||
}
|
||||
|
||||
impl<T: 'static> Plain<T> {
|
||||
pub(crate) fn try_new(inner: Arc<RwLock<T>>) -> Option<Self> {
|
||||
pub fn try_new(inner: Arc<RwLock<T>>) -> Option<Self> {
|
||||
ArcRwLockReadGuardian::take(inner)
|
||||
.ok()
|
||||
.map(|guard| Plain { guard })
|
||||
|
@ -131,7 +134,7 @@ impl<T: 'static> Debug for AsyncPlain<T> {
|
|||
}
|
||||
|
||||
impl<T: 'static> AsyncPlain<T> {
|
||||
pub(crate) fn try_new(inner: &Arc<async_lock::RwLock<T>>) -> Option<Self> {
|
||||
pub fn try_new(inner: &Arc<async_lock::RwLock<T>>) -> Option<Self> {
|
||||
Some(Self {
|
||||
guard: inner.blocking_read_arc(),
|
||||
})
|
||||
|
@ -174,7 +177,7 @@ where
|
|||
}
|
||||
|
||||
impl<T: 'static, U> Mapped<Plain<T>, U> {
|
||||
pub(crate) fn try_new(
|
||||
pub fn try_new(
|
||||
inner: Arc<RwLock<T>>,
|
||||
map_fn: fn(&T) -> &U,
|
||||
) -> Option<Self> {
|
||||
|
@ -187,7 +190,7 @@ impl<Inner, U> Mapped<Inner, U>
|
|||
where
|
||||
Inner: Deref,
|
||||
{
|
||||
pub(crate) fn new_with_guard(
|
||||
pub fn new_with_guard(
|
||||
inner: Inner,
|
||||
map_fn: fn(&Inner::Target) -> &U,
|
||||
) -> Self {
|
||||
|
@ -234,31 +237,37 @@ where
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WriteGuard<'a, S, G>
|
||||
pub struct WriteGuard<S, G>
|
||||
where
|
||||
S: Trigger,
|
||||
{
|
||||
pub(crate) triggerable: Option<&'a S>,
|
||||
pub(crate) triggerable: Option<S>,
|
||||
pub(crate) guard: Option<G>,
|
||||
}
|
||||
|
||||
impl<'a, S, G> WriteGuard<'a, S, G>
|
||||
impl<S, G> WriteGuard<S, G>
|
||||
where
|
||||
S: Trigger,
|
||||
{
|
||||
pub fn new(triggerable: &'a S, guard: G) -> Self {
|
||||
pub fn new(triggerable: S, guard: G) -> Self {
|
||||
Self {
|
||||
triggerable: Some(triggerable),
|
||||
guard: Some(guard),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn untrack(&mut self) {
|
||||
impl<S, G> UntrackableGuard for WriteGuard<S, G>
|
||||
where
|
||||
S: Trigger,
|
||||
G: DerefMut,
|
||||
{
|
||||
fn untrack(&mut self) {
|
||||
self.triggerable.take();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S, G> Deref for WriteGuard<'a, S, G>
|
||||
impl<S, G> Deref for WriteGuard<S, G>
|
||||
where
|
||||
S: Trigger,
|
||||
G: Deref,
|
||||
|
@ -276,7 +285,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, S, G> DerefMut for WriteGuard<'a, S, G>
|
||||
impl<S, G> DerefMut for WriteGuard<S, G>
|
||||
where
|
||||
S: Trigger,
|
||||
G: DerefMut,
|
||||
|
@ -295,7 +304,7 @@ where
|
|||
pub struct UntrackedWriteGuard<T: 'static>(ArcRwLockWriteGuardian<T>);
|
||||
|
||||
impl<T: 'static> UntrackedWriteGuard<T> {
|
||||
pub(crate) fn try_new(inner: Arc<RwLock<T>>) -> Option<Self> {
|
||||
pub fn try_new(inner: Arc<RwLock<T>>) -> Option<Self> {
|
||||
ArcRwLockWriteGuardian::take(inner)
|
||||
.ok()
|
||||
.map(UntrackedWriteGuard)
|
||||
|
@ -317,7 +326,7 @@ impl<T> DerefMut for UntrackedWriteGuard<T> {
|
|||
}
|
||||
|
||||
// Dropping the write guard will notify dependencies.
|
||||
impl<'a, S, T> Drop for WriteGuard<'a, S, T>
|
||||
impl<S, T> Drop for WriteGuard<S, T>
|
||||
where
|
||||
S: Trigger,
|
||||
{
|
||||
|
@ -326,8 +335,82 @@ where
|
|||
drop(self.guard.take());
|
||||
|
||||
// then, notify about a change
|
||||
if let Some(triggerable) = self.triggerable {
|
||||
if let Some(triggerable) = self.triggerable.as_ref() {
|
||||
triggerable.trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MappedMut<Inner, U>
|
||||
where
|
||||
Inner: Deref,
|
||||
{
|
||||
inner: Inner,
|
||||
map_fn: fn(&Inner::Target) -> &U,
|
||||
map_fn_mut: fn(&mut Inner::Target) -> &mut U,
|
||||
}
|
||||
|
||||
impl<Inner, U> UntrackableGuard for MappedMut<Inner, U>
|
||||
where
|
||||
Inner: UntrackableGuard,
|
||||
{
|
||||
fn untrack(&mut self) {
|
||||
self.inner.untrack();
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, U> MappedMut<Inner, U>
|
||||
where
|
||||
Inner: DerefMut,
|
||||
{
|
||||
pub fn new(
|
||||
inner: Inner,
|
||||
map_fn: fn(&Inner::Target) -> &U,
|
||||
map_fn_mut: fn(&mut Inner::Target) -> &mut U,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
map_fn,
|
||||
map_fn_mut,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, U> Deref for MappedMut<Inner, U>
|
||||
where
|
||||
Inner: Deref,
|
||||
{
|
||||
type Target = U;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
(self.map_fn)(self.inner.deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, U> DerefMut for MappedMut<Inner, U>
|
||||
where
|
||||
Inner: DerefMut,
|
||||
{
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
(self.map_fn_mut)(self.inner.deref_mut())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, U: PartialEq> PartialEq for MappedMut<Inner, U>
|
||||
where
|
||||
Inner: Deref,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
**self == **other
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, U: Display> Display for MappedMut<Inner, U>
|
||||
where
|
||||
Inner: Deref,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&**self, f)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ use crate::{
|
|||
owner::StoredValue,
|
||||
signal::guards::{UntrackedWriteGuard, WriteGuard},
|
||||
traits::{
|
||||
DefinedAt, Dispose, IsDisposed, ReadUntracked, Trigger, Writeable,
|
||||
DefinedAt, Dispose, IsDisposed, ReadUntracked, Trigger,
|
||||
UntrackableGuard, Writeable,
|
||||
},
|
||||
unwrap_signal,
|
||||
};
|
||||
|
@ -187,13 +188,11 @@ impl<T: 'static> Trigger for RwSignal<T> {
|
|||
impl<T: 'static> Writeable for RwSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
fn try_write(
|
||||
&self,
|
||||
) -> Option<WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>>> {
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
let guard = self.inner.try_with_value(|n| {
|
||||
ArcRwLockWriteGuardian::take(Arc::clone(&n.value)).ok()
|
||||
})??;
|
||||
Some(WriteGuard::new(self, guard))
|
||||
Some(WriteGuard::new(*self, guard))
|
||||
}
|
||||
|
||||
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>> {
|
||||
|
|
|
@ -4,7 +4,9 @@ use super::{
|
|||
};
|
||||
use crate::{
|
||||
owner::StoredValue,
|
||||
traits::{DefinedAt, Dispose, IsDisposed, Trigger, Writeable},
|
||||
traits::{
|
||||
DefinedAt, Dispose, IsDisposed, Trigger, UntrackableGuard, Writeable,
|
||||
},
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
use guardian::ArcRwLockWriteGuardian;
|
||||
|
@ -83,16 +85,16 @@ impl<T: 'static> Trigger for WriteSignal<T> {
|
|||
impl<T: 'static> Writeable for WriteSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
fn try_write(
|
||||
&self,
|
||||
) -> Option<WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>>> {
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
let guard = self.inner.try_with_value(|n| {
|
||||
ArcRwLockWriteGuardian::take(Arc::clone(&n.value)).ok()
|
||||
})??;
|
||||
Some(WriteGuard::new(self, guard))
|
||||
Some(WriteGuard::new(*self, guard))
|
||||
}
|
||||
|
||||
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>> {
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.inner.with_value(|n| n.try_write_untracked())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,22 +172,24 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub trait UntrackableGuard: DerefMut {
|
||||
fn untrack(&mut self);
|
||||
}
|
||||
|
||||
pub trait Writeable: Sized + DefinedAt + Trigger {
|
||||
type Value: Sized + 'static;
|
||||
|
||||
fn try_write(
|
||||
&self,
|
||||
) -> Option<WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>>>;
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>>;
|
||||
|
||||
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>>;
|
||||
|
||||
fn write(
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>> {
|
||||
) -> Option<impl DerefMut<Target = Self::Value>>;
|
||||
|
||||
fn write(&self) -> impl UntrackableGuard<Target = Self::Value> {
|
||||
self.try_write().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
|
||||
fn write_untracked(&self) -> UntrackedWriteGuard<Self::Value> {
|
||||
fn write_untracked(&self) -> impl DerefMut<Target = Self::Value> {
|
||||
self.try_write_untracked()
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
|
|
18
reactive_stores/Cargo.toml
Normal file
18
reactive_stores/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "reactive_stores"
|
||||
edition = "2021"
|
||||
version = "0.1.0-alpha"
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
guardian = "1.1.0"
|
||||
or_poisoned = { workspace = true }
|
||||
reactive_graph = { workspace = true }
|
||||
rustc-hash = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4" }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
reactive_stores_macro = { workspace = true }
|
||||
reactive_graph = { workspace = true, features = ["effects"] }
|
232
reactive_stores/src/lib.rs
Normal file
232
reactive_stores/src/lib.rs
Normal file
|
@ -0,0 +1,232 @@
|
|||
use reactive_graph::{
|
||||
signal::{
|
||||
guards::{Mapped, Plain, ReadGuard},
|
||||
ArcTrigger,
|
||||
},
|
||||
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Trigger},
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
mod path;
|
||||
mod read_store_field;
|
||||
mod store_field;
|
||||
mod subfield;
|
||||
|
||||
use path::StorePath;
|
||||
use store_field::StoreField;
|
||||
pub use subfield::Subfield;
|
||||
|
||||
pub struct ArcStore<T> {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
pub(crate) value: Arc<RwLock<T>>,
|
||||
signals: Arc<RwLock<TriggerMap>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct TriggerMap(FxHashMap<StorePath, ArcTrigger>);
|
||||
|
||||
impl TriggerMap {
|
||||
fn get_or_insert(&mut self, key: StorePath) -> ArcTrigger {
|
||||
if let Some(trigger) = self.0.get(&key) {
|
||||
trigger.clone()
|
||||
} else {
|
||||
let new = ArcTrigger::new();
|
||||
self.0.insert(key, new.clone());
|
||||
new
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&mut self, key: &StorePath) -> Option<ArcTrigger> {
|
||||
self.0.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ArcStore<T> {
|
||||
pub fn new(value: T) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
value: Arc::new(RwLock::new(value)),
|
||||
signals: Default::default(),
|
||||
/* inner: Arc::new(RwLock::new(SubscriberSet::new())), */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for ArcStore<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut f = f.debug_struct("ArcStore");
|
||||
#[cfg(debug_assertions)]
|
||||
let f = f.field("defined_at", &self.defined_at);
|
||||
f.field("value", &self.value)
|
||||
.field("signals", &self.signals)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for ArcStore<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
value: Arc::clone(&self.value),
|
||||
signals: Arc::clone(&self.signals),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DefinedAt for ArcStore<T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IsDisposed for ArcStore<T> {
|
||||
#[inline(always)]
|
||||
fn is_disposed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ReadUntracked for ArcStore<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = ReadGuard<T, Plain<T>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
Plain::try_new(Arc::clone(&self.value)).map(ReadGuard::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Track for ArcStore<T> {
|
||||
fn track(&self) {
|
||||
self.get_trigger(Default::default()).trigger();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Trigger for ArcStore<T> {
|
||||
fn trigger(&self) {
|
||||
self.get_trigger(self.path().collect()).trigger();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ArcStore;
|
||||
use crate as reactive_stores;
|
||||
use reactive_graph::{
|
||||
effect::Effect,
|
||||
traits::{Read, ReadUntracked, Set, Update, Writeable},
|
||||
};
|
||||
use reactive_stores_macro::Store;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
pub async fn tick() {
|
||||
tokio::time::sleep(std::time::Duration::from_micros(1)).await;
|
||||
}
|
||||
|
||||
#[derive(Debug, Store)]
|
||||
struct Todos {
|
||||
user: String,
|
||||
todos: Vec<Todo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Store)]
|
||||
struct Todo {
|
||||
label: String,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(label: impl ToString) -> Self {
|
||||
Self {
|
||||
label: label.to_string(),
|
||||
completed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn data() -> Todos {
|
||||
Todos {
|
||||
user: "Bob".to_string(),
|
||||
todos: vec![
|
||||
Todo {
|
||||
label: "Create reactive store".to_string(),
|
||||
completed: true,
|
||||
},
|
||||
Todo {
|
||||
label: "???".to_string(),
|
||||
completed: false,
|
||||
},
|
||||
Todo {
|
||||
label: "Profit".to_string(),
|
||||
completed: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mutating_field_triggers_effect() {
|
||||
_ = any_spawner::Executor::init_tokio();
|
||||
|
||||
let combined_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let store = ArcStore::new(data());
|
||||
assert_eq!(store.read_untracked().todos.len(), 3);
|
||||
assert_eq!(store.clone().user().read_untracked().as_str(), "Bob");
|
||||
Effect::new_sync({
|
||||
let store = store.clone();
|
||||
let combined_count = Arc::clone(&combined_count);
|
||||
move |prev| {
|
||||
if prev.is_none() {
|
||||
println!("first run");
|
||||
} else {
|
||||
println!("next run");
|
||||
}
|
||||
println!("{:?}", *store.clone().user().read());
|
||||
combined_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
tick().await;
|
||||
tick().await;
|
||||
store.clone().user().set("Greg".into());
|
||||
tick().await;
|
||||
store.clone().user().set("Carol".into());
|
||||
tick().await;
|
||||
store.clone().user().update(|name| name.push_str("!!!"));
|
||||
tick().await;
|
||||
// the effect reads from `user`, so it should trigger every time
|
||||
assert_eq!(combined_count.load(Ordering::Relaxed), 4);
|
||||
|
||||
store
|
||||
.clone()
|
||||
.todos()
|
||||
.write()
|
||||
.push(Todo::new("Create reactive stores"));
|
||||
tick().await;
|
||||
store.clone().todos().write().push(Todo::new("???"));
|
||||
tick().await;
|
||||
store.clone().todos().write().push(Todo::new("Profit!"));
|
||||
tick().await;
|
||||
// the effect doesn't read from `todos`, so the count should not have changed
|
||||
assert_eq!(combined_count.load(Ordering::Relaxed), 4);
|
||||
}
|
||||
}
|
39
reactive_stores/src/path.rs
Normal file
39
reactive_stores/src/path.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct StorePath(Vec<StorePathSegment>);
|
||||
|
||||
impl From<Vec<StorePathSegment>> for StorePath {
|
||||
fn from(value: Vec<StorePathSegment>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl StorePath {
|
||||
pub fn push(&mut self, segment: impl Into<StorePathSegment>) {
|
||||
self.0.push(segment.into());
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<StorePathSegment> {
|
||||
self.0.pop()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct StorePathSegment(usize);
|
||||
|
||||
impl From<usize> for StorePathSegment {
|
||||
fn from(value: usize) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&usize> for StorePathSegment {
|
||||
fn from(value: &usize) -> Self {
|
||||
Self(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<StorePathSegment> for StorePath {
|
||||
fn from_iter<T: IntoIterator<Item = StorePathSegment>>(iter: T) -> Self {
|
||||
Self(Vec::from_iter(iter))
|
||||
}
|
||||
}
|
64
reactive_stores/src/read_store_field.rs
Normal file
64
reactive_stores/src/read_store_field.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use reactive_graph::{
|
||||
signal::{
|
||||
guards::{Mapped, Plain, ReadGuard},
|
||||
ArcTrigger,
|
||||
},
|
||||
traits::{DefinedAt, ReadUntracked, Track},
|
||||
};
|
||||
use std::{
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
pub struct ArcReadStoreField<Orig, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
data: Arc<RwLock<Orig>>,
|
||||
trigger: ArcTrigger,
|
||||
read: fn(&Orig) -> &T,
|
||||
}
|
||||
|
||||
impl<Orig, T> Clone for ArcReadStoreField<Orig, T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
data: Arc::clone(&self.data),
|
||||
trigger: self.trigger.clone(),
|
||||
read: self.read,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Orig, T> DefinedAt for ArcReadStoreField<Orig, T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Orig, T> Track for ArcReadStoreField<Orig, T> {
|
||||
fn track(&self) {
|
||||
self.trigger.track();
|
||||
}
|
||||
}
|
||||
|
||||
impl<Orig, T> ReadUntracked for ArcReadStoreField<Orig, T>
|
||||
where
|
||||
Orig: 'static,
|
||||
{
|
||||
type Value = ReadGuard<T, Mapped<Plain<Orig>, T>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
Mapped::try_new(Arc::clone(&self.data), self.read).map(ReadGuard::new)
|
||||
}
|
||||
}
|
68
reactive_stores/src/store_field.rs
Normal file
68
reactive_stores/src/store_field.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use crate::{
|
||||
path::{StorePath, StorePathSegment},
|
||||
ArcStore,
|
||||
};
|
||||
use guardian::ArcRwLockWriteGuardian;
|
||||
use or_poisoned::OrPoisoned;
|
||||
use reactive_graph::{
|
||||
signal::{
|
||||
guards::{Plain, WriteGuard},
|
||||
ArcTrigger,
|
||||
},
|
||||
traits::UntrackableGuard,
|
||||
};
|
||||
use std::{
|
||||
iter,
|
||||
ops::Deref,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
pub trait StoreField<T>: Sized {
|
||||
type Orig;
|
||||
type Reader: Deref<Target = T>;
|
||||
type Writer: UntrackableGuard<Target = T>;
|
||||
|
||||
fn data(&self) -> Arc<RwLock<Self::Orig>>;
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> ArcTrigger;
|
||||
|
||||
fn path(&self) -> impl Iterator<Item = StorePathSegment>;
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader>;
|
||||
|
||||
fn writer(&self) -> Option<Self::Writer>;
|
||||
}
|
||||
|
||||
impl<T> StoreField<T> for ArcStore<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Orig = T;
|
||||
type Reader = Plain<T>;
|
||||
type Writer = WriteGuard<ArcTrigger, ArcRwLockWriteGuardian<T>>;
|
||||
|
||||
fn data(&self) -> Arc<RwLock<Self::Orig>> {
|
||||
Arc::clone(&self.value)
|
||||
}
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> ArcTrigger {
|
||||
let triggers = &self.signals;
|
||||
let trigger = triggers.write().or_poisoned().get_or_insert(path);
|
||||
trigger
|
||||
}
|
||||
|
||||
fn path(&self) -> impl Iterator<Item = StorePathSegment> {
|
||||
iter::empty()
|
||||
}
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
Plain::try_new(Arc::clone(&self.value))
|
||||
}
|
||||
|
||||
fn writer(&self) -> Option<Self::Writer> {
|
||||
let trigger = self.get_trigger(Default::default());
|
||||
let guard =
|
||||
ArcRwLockWriteGuardian::take(Arc::clone(&self.value)).ok()?;
|
||||
Some(WriteGuard::new(trigger, guard))
|
||||
}
|
||||
}
|
194
reactive_stores/src/subfield.rs
Normal file
194
reactive_stores/src/subfield.rs
Normal file
|
@ -0,0 +1,194 @@
|
|||
use crate::{
|
||||
path::{StorePath, StorePathSegment},
|
||||
store_field::StoreField,
|
||||
};
|
||||
use reactive_graph::{
|
||||
signal::{
|
||||
guards::{
|
||||
Mapped, MappedMut, Plain, ReadGuard, UntrackedWriteGuard,
|
||||
WriteGuard,
|
||||
},
|
||||
ArcTrigger,
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, IsDisposed, ReadUntracked, Track, Trigger, UntrackableGuard,
|
||||
Writeable,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
iter,
|
||||
marker::PhantomData,
|
||||
ops::DerefMut,
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Subfield<Inner, Prev, T>
|
||||
where
|
||||
Inner: StoreField<Prev>,
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
path_segment: StorePathSegment,
|
||||
inner: Inner,
|
||||
read: fn(&Prev) -> &T,
|
||||
write: fn(&mut Prev) -> &mut T,
|
||||
ty: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> Clone for Subfield<Inner, Prev, T>
|
||||
where
|
||||
Inner: StoreField<Prev> + Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
path_segment: self.path_segment,
|
||||
inner: self.inner.clone(),
|
||||
read: self.read,
|
||||
write: self.write,
|
||||
ty: self.ty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> Copy for Subfield<Inner, Prev, T> where
|
||||
Inner: StoreField<Prev> + Copy
|
||||
{
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> Subfield<Inner, Prev, T>
|
||||
where
|
||||
Inner: StoreField<Prev>,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new(
|
||||
inner: Inner,
|
||||
path_segment: StorePathSegment,
|
||||
read: fn(&Prev) -> &T,
|
||||
write: fn(&mut Prev) -> &mut T,
|
||||
) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner,
|
||||
path_segment,
|
||||
read,
|
||||
write,
|
||||
ty: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> StoreField<T> for Subfield<Inner, Prev, T>
|
||||
where
|
||||
Inner: StoreField<Prev>,
|
||||
{
|
||||
type Orig = Inner::Orig;
|
||||
type Reader = Mapped<Inner::Reader, T>;
|
||||
type Writer = MappedMut<WriteGuard<ArcTrigger, Inner::Writer>, T>;
|
||||
|
||||
fn path(&self) -> impl Iterator<Item = StorePathSegment> {
|
||||
self.inner.path().chain(iter::once(self.path_segment))
|
||||
}
|
||||
|
||||
fn data(&self) -> Arc<RwLock<Self::Orig>> {
|
||||
self.inner.data()
|
||||
}
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> ArcTrigger {
|
||||
self.inner.get_trigger(path)
|
||||
}
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
let inner = self.inner.reader()?;
|
||||
Some(Mapped::new_with_guard(inner, self.read))
|
||||
}
|
||||
|
||||
fn writer(&self) -> Option<Self::Writer> {
|
||||
let trigger = self.get_trigger(self.path().collect());
|
||||
let inner = WriteGuard::new(trigger, self.inner.writer()?);
|
||||
Some(MappedMut::new(inner, self.read, self.write))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> DefinedAt for Subfield<Inner, Prev, T>
|
||||
where
|
||||
Inner: StoreField<Prev>,
|
||||
{
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> IsDisposed for Subfield<Inner, Prev, T>
|
||||
where
|
||||
Inner: StoreField<Prev> + IsDisposed,
|
||||
{
|
||||
fn is_disposed(&self) -> bool {
|
||||
self.inner.is_disposed()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> Trigger for Subfield<Inner, Prev, T>
|
||||
where
|
||||
Inner: StoreField<Prev>,
|
||||
{
|
||||
fn trigger(&self) {
|
||||
let trigger = self.get_trigger(self.path().collect());
|
||||
trigger.trigger();
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> Track for Subfield<Inner, Prev, T>
|
||||
where
|
||||
Inner: StoreField<Prev> + Send + Sync + Clone + 'static,
|
||||
Prev: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn track(&self) {
|
||||
let trigger = self.get_trigger(self.path().collect());
|
||||
trigger.track();
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> ReadUntracked for Subfield<Inner, Prev, T>
|
||||
where
|
||||
Inner: StoreField<Prev>,
|
||||
{
|
||||
type Value = <Self as StoreField<T>>::Reader;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
self.reader()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> Writeable for Subfield<Inner, Prev, T>
|
||||
where
|
||||
T: 'static,
|
||||
Inner: StoreField<Prev>,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
self.writer()
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.writer().map(|mut writer| {
|
||||
writer.untrack();
|
||||
writer
|
||||
})
|
||||
}
|
||||
}
|
14
reactive_stores_macro/Cargo.toml
Normal file
14
reactive_stores_macro/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "reactive_stores_macro"
|
||||
edition = "2021"
|
||||
version = "0.1.0-alpha"
|
||||
rust-version.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro-error = "1"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = "2"
|
211
reactive_stores_macro/src/lib.rs
Normal file
211
reactive_stores_macro/src/lib.rs
Normal file
|
@ -0,0 +1,211 @@
|
|||
use proc_macro2::{Span, TokenStream};
|
||||
use proc_macro_error::{abort, abort_call_site, proc_macro_error};
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream, Parser},
|
||||
punctuated::Punctuated,
|
||||
token::Comma,
|
||||
Data, Field, Fields, Generics, Ident, Meta, MetaList, Result, Visibility,
|
||||
WhereClause, Type, Token, Index,
|
||||
};
|
||||
|
||||
#[proc_macro_error]
|
||||
#[proc_macro_derive(Store, attributes(store))]
|
||||
pub fn derive_store(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
syn::parse_macro_input!(input as Model)
|
||||
.into_token_stream()
|
||||
.into()
|
||||
}
|
||||
|
||||
struct Model {
|
||||
pub vis: Visibility,
|
||||
pub struct_name: Ident,
|
||||
pub generics: Generics,
|
||||
pub fields: Vec<Field>,
|
||||
}
|
||||
|
||||
impl Parse for Model {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let input = syn::DeriveInput::parse(input)?;
|
||||
|
||||
let syn::Data::Struct(s) = input.data else {
|
||||
abort_call_site!("only structs can be used with `Store`");
|
||||
};
|
||||
|
||||
let fields = match s.fields {
|
||||
syn::Fields::Unit => {
|
||||
abort!(s.semi_token, "unit structs are not supported");
|
||||
}
|
||||
syn::Fields::Named(fields) => {
|
||||
fields.named.into_iter().collect::<Vec<_>>()
|
||||
}
|
||||
syn::Fields::Unnamed(fields) => fields
|
||||
.unnamed
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
vis: input.vis,
|
||||
struct_name: input.ident,
|
||||
generics: input.generics,
|
||||
fields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum SubfieldMode {
|
||||
Keyed(Ident, Type),
|
||||
}
|
||||
|
||||
impl Parse for SubfieldMode {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let mode: Ident = input.parse()?;
|
||||
if mode == "key" {
|
||||
let _eq: Token!(=) = input.parse()?;
|
||||
let ident: Ident = input.parse()?;
|
||||
let _col: Token!(:) = input.parse()?;
|
||||
let ty: Type = input.parse()?;
|
||||
Ok(SubfieldMode::Keyed(ident, ty))
|
||||
} else {
|
||||
Err(input.error("expected `key = <ident>: <Type>`"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn field_to_tokens(idx: usize, include_body: bool, modes: Option<&[SubfieldMode]>, library_path: &proc_macro2::TokenStream, orig_ident: Option<&Ident>, generics: &Generics, any_store_field: &Ident, struct_name: &Ident, ty: &Type) -> proc_macro2::TokenStream {
|
||||
let ident = if orig_ident.is_none() {
|
||||
let idx = Ident::new(&format!("field{idx}"), Span::call_site());
|
||||
quote! { #idx }
|
||||
} else {
|
||||
quote! { #orig_ident }
|
||||
};
|
||||
let locator = if orig_ident.is_none() {
|
||||
let idx = Index::from(idx);
|
||||
quote! { #idx }
|
||||
} else {
|
||||
quote! { #ident }
|
||||
};
|
||||
|
||||
if let Some(modes) = modes {
|
||||
if modes.len() == 1 {
|
||||
let mode = &modes[0];
|
||||
// Can replace with a match if additional modes added
|
||||
let SubfieldMode::Keyed(keyed_by, key_ty) = mode;
|
||||
let signature = quote! {
|
||||
fn #ident(self) -> #library_path::KeyedField<#any_store_field, #struct_name #generics, #ty, #key_ty>
|
||||
};
|
||||
return if include_body {
|
||||
quote! {
|
||||
#signature {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! { #signature; }
|
||||
}
|
||||
} else {
|
||||
abort!(orig_ident.map(|ident| ident.span()).unwrap_or_else(Span::call_site), "multiple modes not currently supported");
|
||||
}
|
||||
}
|
||||
|
||||
// default subfield
|
||||
if include_body {
|
||||
quote! {
|
||||
fn #ident(self) -> #library_path::Subfield<#any_store_field, #struct_name #generics, #ty> {
|
||||
#library_path::Subfield::new(
|
||||
self,
|
||||
#idx.into(),
|
||||
|prev| &prev.#locator,
|
||||
|prev| &mut prev.#locator,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
fn #ident(self) -> #library_path::Subfield<#any_store_field, #struct_name #generics, #ty>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Model {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let library_path = quote! { reactive_stores };
|
||||
let Model {
|
||||
vis,
|
||||
struct_name,
|
||||
generics,
|
||||
fields,
|
||||
} = &self;
|
||||
let any_store_field = Ident::new("AnyStoreField", Span::call_site());
|
||||
let trait_name = Ident::new(
|
||||
&format!("{struct_name}StoreFields"),
|
||||
struct_name.span(),
|
||||
);
|
||||
let generics_with_orig = {
|
||||
let params = &generics.params;
|
||||
quote! { <#any_store_field, #params> }
|
||||
};
|
||||
let where_with_orig = {
|
||||
generics
|
||||
.where_clause
|
||||
.as_ref()
|
||||
.map(|w| {
|
||||
let WhereClause {
|
||||
where_token,
|
||||
predicates,
|
||||
} = &w;
|
||||
quote! {
|
||||
#where_token
|
||||
#any_store_field: #library_path::StoreField<#struct_name #generics>,
|
||||
#predicates
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| quote! { where #any_store_field: #library_path::StoreField<#struct_name #generics> })
|
||||
};
|
||||
|
||||
// define an extension trait that matches this struct
|
||||
let all_field_data = fields.iter().enumerate().map(|(idx, field)| {
|
||||
let Field { ident, ty, attrs, .. } = &field;
|
||||
let modes = attrs.iter().find_map(|attr| {
|
||||
attr.meta.path().is_ident("store").then(|| {
|
||||
match &attr.meta {
|
||||
Meta::List(list) => {
|
||||
match Punctuated::<SubfieldMode, Comma>::parse_terminated.parse2(list.tokens.clone()) {
|
||||
Ok(modes) => Some(modes.iter().cloned().collect::<Vec<_>>()),
|
||||
Err(e) => abort!(list, e)
|
||||
}
|
||||
},
|
||||
_ => None
|
||||
}
|
||||
})
|
||||
}).flatten();
|
||||
|
||||
(
|
||||
field_to_tokens(idx, false, modes.as_deref(), &library_path, ident.as_ref(), generics, &any_store_field, struct_name, ty),
|
||||
field_to_tokens(idx, true, modes.as_deref(), &library_path, ident.as_ref(), generics, &any_store_field, struct_name, ty),
|
||||
)
|
||||
|
||||
});
|
||||
|
||||
// implement that trait for all StoreFields
|
||||
let (trait_fields, read_fields): (Vec<_>, Vec<_>) = all_field_data.unzip();
|
||||
|
||||
// read access
|
||||
tokens.extend(quote! {
|
||||
#vis trait #trait_name <AnyStoreField>
|
||||
#where_with_orig
|
||||
{
|
||||
#(#trait_fields)*
|
||||
}
|
||||
|
||||
impl #generics_with_orig #trait_name <AnyStoreField> for AnyStoreField
|
||||
#where_with_orig
|
||||
{
|
||||
#(#read_fields)*
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue