feat: add support for simple enums in stores
This commit is contained in:
parent
1ad3249764
commit
0ce0184e8c
|
@ -1,7 +1,12 @@
|
|||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::{Field, Store, StoreFieldIterator};
|
||||
use reactive_stores_macro::Store;
|
||||
|
||||
// ID starts higher than 0 because we have a few starting todos by default
|
||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(3);
|
||||
|
||||
#[derive(Debug, Store)]
|
||||
struct Todos {
|
||||
user: String,
|
||||
|
@ -10,15 +15,35 @@ struct Todos {
|
|||
|
||||
#[derive(Debug, Store)]
|
||||
struct Todo {
|
||||
id: usize,
|
||||
label: String,
|
||||
completed: bool,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Store)]
|
||||
enum Status {
|
||||
#[default]
|
||||
Pending,
|
||||
Scheduled,
|
||||
Done,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn next_step(&mut self) {
|
||||
*self = match self {
|
||||
Status::Pending => Status::Scheduled,
|
||||
Status::Scheduled => Status::Done,
|
||||
Status::Done => Status::Done,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(label: impl ToString) -> Self {
|
||||
Self {
|
||||
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
|
||||
label: label.to_string(),
|
||||
completed: false,
|
||||
status: Status::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,16 +53,19 @@ fn data() -> Todos {
|
|||
user: "Bob".to_string(),
|
||||
todos: vec![
|
||||
Todo {
|
||||
id: 0,
|
||||
label: "Create reactive store".to_string(),
|
||||
completed: true,
|
||||
status: Status::Pending,
|
||||
},
|
||||
Todo {
|
||||
id: 1,
|
||||
label: "???".to_string(),
|
||||
completed: false,
|
||||
status: Status::Pending,
|
||||
},
|
||||
Todo {
|
||||
id: 2,
|
||||
label: "Profit".to_string(),
|
||||
completed: false,
|
||||
status: Status::Pending,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -49,15 +77,6 @@ pub fn App() -> impl IntoView {
|
|||
|
||||
let input_ref = NodeRef::new();
|
||||
|
||||
let rows = move || {
|
||||
store
|
||||
.todos()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, todo)| view! { <TodoRow store idx todo/> })
|
||||
.collect_view()
|
||||
};
|
||||
|
||||
view! {
|
||||
<p>"Hello, " {move || store.user().get()}</p>
|
||||
<form on:submit=move |ev| {
|
||||
|
@ -67,7 +86,12 @@ pub fn App() -> impl IntoView {
|
|||
<label>"Add a Todo" <input type="text" node_ref=input_ref/></label>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
<ol>{rows}</ol>
|
||||
<ol>
|
||||
<For each=move || store.todos().iter() key=|row| row.id().get() let:todo>
|
||||
<TodoRow store todo/>
|
||||
</For>
|
||||
|
||||
</ol>
|
||||
<div style="display: flex"></div>
|
||||
}
|
||||
}
|
||||
|
@ -75,22 +99,18 @@ pub fn App() -> impl IntoView {
|
|||
#[component]
|
||||
fn TodoRow(
|
||||
store: Store<Todos>,
|
||||
idx: usize,
|
||||
#[prop(into)] todo: Field<Todo>,
|
||||
) -> impl IntoView {
|
||||
let completed = todo.completed();
|
||||
let status = todo.status();
|
||||
let title = todo.label();
|
||||
|
||||
let editing = RwSignal::new(false);
|
||||
|
||||
view! {
|
||||
<li
|
||||
style:text-decoration=move || {
|
||||
completed.get().then_some("line-through").unwrap_or_default()
|
||||
}
|
||||
<li style:text-decoration=move || {
|
||||
status.done().then_some("line-through").unwrap_or_default()
|
||||
}>
|
||||
|
||||
class:foo=move || completed.get()
|
||||
>
|
||||
<p
|
||||
class:hidden=move || editing.get()
|
||||
on:click=move |_| {
|
||||
|
@ -112,17 +132,26 @@ fn TodoRow(
|
|||
on:blur=move |_| editing.set(false)
|
||||
autofocus
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
prop:checked=move || completed.get()
|
||||
on:click=move |_| { completed.update(|n| *n = !*n) }
|
||||
/>
|
||||
<button on:click=move |_| {
|
||||
status.write().next_step()
|
||||
}>
|
||||
{move || {
|
||||
if todo.status().done() {
|
||||
"Done"
|
||||
} else if status.scheduled() {
|
||||
"Scheduled"
|
||||
} else {
|
||||
"Pending"
|
||||
}
|
||||
}}
|
||||
|
||||
</button>
|
||||
|
||||
<button on:click=move |_| {
|
||||
store
|
||||
.todos()
|
||||
.update(|todos| {
|
||||
todos.remove(idx);
|
||||
todos.remove(todo.id().get());
|
||||
});
|
||||
}>"X"</button>
|
||||
</li>
|
||||
|
|
|
@ -28,7 +28,7 @@ pub use iter::*;
|
|||
pub use option::*;
|
||||
pub use patch::*;
|
||||
use path::StorePath;
|
||||
pub use store_field::StoreField;
|
||||
pub use store_field::{StoreField, Then};
|
||||
pub use subfield::Subfield;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
|
@ -139,6 +139,26 @@ where
|
|||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<T, S> Then<T, S>
|
||||
where
|
||||
S: StoreField,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new(
|
||||
inner: S,
|
||||
map_fn: fn(&S::Value) -> &T,
|
||||
map_fn_mut: fn(&mut S::Value) -> &mut T,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
map_fn,
|
||||
map_fn_mut,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> StoreField for Then<T, S>
|
||||
where
|
||||
S: StoreField,
|
||||
|
|
|
@ -8,6 +8,7 @@ edition.workspace = true
|
|||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
convert_case = "0.6"
|
||||
proc-macro-error = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use proc_macro2::Span;
|
||||
use convert_case::{Case, Casing};
|
||||
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,
|
||||
Field, Generics, Ident, Index, Meta, Result, Token, Type, Visibility,
|
||||
WhereClause,
|
||||
Field, Fields, Generics, Ident, Index, Meta, Result, Token, Type, Variant,
|
||||
Visibility, WhereClause,
|
||||
};
|
||||
|
||||
#[proc_macro_error]
|
||||
|
@ -26,20 +27,23 @@ pub fn derive_patch(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
|||
}
|
||||
|
||||
struct Model {
|
||||
pub vis: Visibility,
|
||||
pub struct_name: Ident,
|
||||
pub generics: Generics,
|
||||
pub fields: Vec<Field>,
|
||||
vis: Visibility,
|
||||
name: Ident,
|
||||
generics: Generics,
|
||||
ty: ModelTy,
|
||||
}
|
||||
|
||||
enum ModelTy {
|
||||
Struct { fields: Vec<Field> },
|
||||
Enum { variants: Vec<Variant> },
|
||||
}
|
||||
|
||||
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 ty = match input.data {
|
||||
syn::Data::Struct(s) => {
|
||||
let fields = match s.fields {
|
||||
syn::Fields::Unit => {
|
||||
abort!(s.semi_token, "unit structs are not supported");
|
||||
|
@ -52,11 +56,23 @@ impl Parse for Model {
|
|||
}
|
||||
};
|
||||
|
||||
ModelTy::Struct { fields }
|
||||
}
|
||||
syn::Data::Enum(e) => ModelTy::Enum {
|
||||
variants: e.variants.into_iter().collect(),
|
||||
},
|
||||
_ => {
|
||||
abort_call_site!(
|
||||
"only structs and enums can be used with `Store`"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
vis: input.vis,
|
||||
struct_name: input.ident,
|
||||
generics: input.generics,
|
||||
fields,
|
||||
name: input.ident,
|
||||
ty,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +97,164 @@ impl Parse for SubfieldMode {
|
|||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Model {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let library_path = quote! { reactive_stores };
|
||||
let Model {
|
||||
vis,
|
||||
name,
|
||||
generics,
|
||||
ty,
|
||||
} = &self;
|
||||
let any_store_field = Ident::new("AnyStoreField", Span::call_site());
|
||||
let trait_name = Ident::new(&format!("{name}StoreFields"), 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<Value = #name #generics>,
|
||||
#predicates
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| quote! { where #any_store_field: #library_path::StoreField<Value = #name #generics> })
|
||||
};
|
||||
|
||||
// define an extension trait that matches this struct
|
||||
// and implement that trait for all StoreFields
|
||||
let (trait_fields, read_fields): (Vec<_>, Vec<_>) =
|
||||
ty.to_field_data(&library_path, generics, &any_store_field, name);
|
||||
|
||||
// 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)*
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelTy {
|
||||
fn to_field_data(
|
||||
&self,
|
||||
library_path: &TokenStream,
|
||||
generics: &Generics,
|
||||
any_store_field: &Ident,
|
||||
name: &Ident,
|
||||
) -> (Vec<TokenStream>, Vec<TokenStream>) {
|
||||
match self {
|
||||
ModelTy::Struct { fields } => 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,
|
||||
name,
|
||||
ty,
|
||||
),
|
||||
field_to_tokens(
|
||||
idx,
|
||||
true,
|
||||
modes.as_deref(),
|
||||
library_path,
|
||||
ident.as_ref(),
|
||||
generics,
|
||||
any_store_field,
|
||||
name,
|
||||
ty,
|
||||
),
|
||||
)
|
||||
})
|
||||
.unzip(),
|
||||
ModelTy::Enum { variants } => variants
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, variant)| {
|
||||
let Variant { ident, fields, .. } = variant;
|
||||
|
||||
(
|
||||
variant_to_tokens(
|
||||
idx,
|
||||
false,
|
||||
library_path,
|
||||
ident,
|
||||
generics,
|
||||
any_store_field,
|
||||
name,
|
||||
fields,
|
||||
),
|
||||
variant_to_tokens(
|
||||
idx,
|
||||
true,
|
||||
library_path,
|
||||
ident,
|
||||
generics,
|
||||
any_store_field,
|
||||
name,
|
||||
fields,
|
||||
),
|
||||
)
|
||||
})
|
||||
.unzip(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn field_to_tokens(
|
||||
idx: usize,
|
||||
|
@ -90,7 +264,7 @@ fn field_to_tokens(
|
|||
orig_ident: Option<&Ident>,
|
||||
generics: &Generics,
|
||||
any_store_field: &Ident,
|
||||
struct_name: &Ident,
|
||||
name: &Ident,
|
||||
ty: &Type,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let ident = if orig_ident.is_none() {
|
||||
|
@ -113,7 +287,7 @@ fn field_to_tokens(
|
|||
// TODO keyed_by
|
||||
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>
|
||||
fn #ident(self) -> #library_path::KeyedField<#any_store_field, #name #generics, #ty, #key_ty>
|
||||
};
|
||||
return if include_body {
|
||||
quote! {
|
||||
|
@ -137,7 +311,7 @@ fn field_to_tokens(
|
|||
// default subfield
|
||||
if include_body {
|
||||
quote! {
|
||||
fn #ident(self) -> #library_path::Subfield<#any_store_field, #struct_name #generics, #ty> {
|
||||
fn #ident(self) -> #library_path::Subfield<#any_store_field, #name #generics, #ty> {
|
||||
#library_path::Subfield::new(
|
||||
self,
|
||||
#idx.into(),
|
||||
|
@ -148,93 +322,59 @@ fn field_to_tokens(
|
|||
}
|
||||
} else {
|
||||
quote! {
|
||||
fn #ident(self) -> #library_path::Subfield<#any_store_field, #struct_name #generics, #ty>;
|
||||
fn #ident(self) -> #library_path::Subfield<#any_store_field, #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;
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn variant_to_tokens(
|
||||
idx: usize,
|
||||
include_body: bool,
|
||||
library_path: &proc_macro2::TokenStream,
|
||||
ident: &Ident,
|
||||
generics: &Generics,
|
||||
any_store_field: &Ident,
|
||||
name: &Ident,
|
||||
fields: &Fields,
|
||||
) -> proc_macro2::TokenStream {
|
||||
// the method name will always be the snake_cased ident
|
||||
let orig_ident = &ident;
|
||||
let ident =
|
||||
Ident::new(&ident.to_string().to_case(Case::Snake), ident.span());
|
||||
|
||||
match fields {
|
||||
// For unit enum fields, we will just return a `bool` subfield, which is
|
||||
// true when this field matches
|
||||
Fields::Unit => {
|
||||
// default subfield
|
||||
if include_body {
|
||||
quote! {
|
||||
#where_token
|
||||
#any_store_field: #library_path::StoreField<Value = #struct_name #generics>,
|
||||
#predicates
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| quote! { where #any_store_field: #library_path::StoreField<Value = #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)
|
||||
}
|
||||
fn #ident(&self) -> bool {
|
||||
match #library_path::StoreField::reader(self) {
|
||||
Some(reader) => {
|
||||
let path = #library_path::StoreField::path(self).into_iter().collect();
|
||||
let trigger = #library_path::StoreField::get_trigger(self, path);
|
||||
trigger.track();
|
||||
matches!(&*reader, #name::#orig_ident)
|
||||
},
|
||||
_ => None
|
||||
None => false
|
||||
}
|
||||
})
|
||||
}).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)*
|
||||
}
|
||||
});
|
||||
} else {
|
||||
quote! {
|
||||
fn #ident(&self) -> bool;
|
||||
}
|
||||
}
|
||||
}
|
||||
Fields::Named(_) => todo!(),
|
||||
Fields::Unnamed(_) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
struct PatchModel {
|
||||
pub struct_name: Ident,
|
||||
pub name: Ident,
|
||||
pub generics: Generics,
|
||||
pub fields: Vec<Field>,
|
||||
}
|
||||
|
@ -260,7 +400,7 @@ impl Parse for PatchModel {
|
|||
};
|
||||
|
||||
Ok(Self {
|
||||
struct_name: input.ident,
|
||||
name: input.ident,
|
||||
generics: input.generics,
|
||||
fields,
|
||||
})
|
||||
|
@ -271,7 +411,7 @@ impl ToTokens for PatchModel {
|
|||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let library_path = quote! { reactive_stores };
|
||||
let PatchModel {
|
||||
struct_name,
|
||||
name,
|
||||
generics,
|
||||
fields,
|
||||
} = &self;
|
||||
|
@ -294,7 +434,7 @@ impl ToTokens for PatchModel {
|
|||
|
||||
// read access
|
||||
tokens.extend(quote! {
|
||||
impl #library_path::PatchField for #struct_name #generics
|
||||
impl #library_path::PatchField for #name #generics
|
||||
{
|
||||
fn patch_field(
|
||||
&mut self,
|
||||
|
|
Loading…
Reference in New Issue