173 lines
5.6 KiB
Rust
173 lines
5.6 KiB
Rust
mod edit;
|
|
|
|
use convert_case::{Case, Casing};
|
|
use proc_macro::TokenStream;
|
|
use quote::{format_ident, quote};
|
|
use syn::Fields;
|
|
|
|
#[proc_macro_derive(WebEdit)]
|
|
pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
|
let input: syn::DeriveInput = syn::parse(tokens).unwrap();
|
|
|
|
let name = input.ident;
|
|
let edit_ident = format_ident!("{}Edit", name);
|
|
|
|
let editable = quote! {
|
|
pub struct #edit_ident;
|
|
|
|
impl<'a, G: Html> Editable<'a, G> for #name {
|
|
type Editor = #edit_ident;
|
|
}
|
|
};
|
|
|
|
let derived = match input.data {
|
|
syn::Data::Struct(s) => {
|
|
let fields = s.fields.iter().map(|f| {
|
|
let name = f
|
|
.ident
|
|
.as_ref()
|
|
.expect("each struct field must be named")
|
|
.clone();
|
|
let name_str = name.to_string();
|
|
(name_str, name)
|
|
});
|
|
|
|
let fields_view = fields.clone().map(|(name_str, name)| {
|
|
let title = name_str.to_case(Case::Title);
|
|
quote! {
|
|
p {
|
|
label { (#title) }
|
|
(#name.edit(cx))
|
|
}
|
|
}
|
|
});
|
|
|
|
let signals = fields.clone().map(|(_name_str, name)| {
|
|
quote! {
|
|
let #name = create_signal(cx, state.get_untracked().#name.clone());
|
|
}
|
|
});
|
|
|
|
let effect_fields = fields.clone().map(|(_name_str, name)| {
|
|
quote! {
|
|
#name: #name.get().as_ref().clone()
|
|
}
|
|
});
|
|
|
|
quote! {
|
|
impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident {
|
|
fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View<G> {
|
|
let state = props.state;
|
|
|
|
#(#signals)*
|
|
|
|
create_effect(cx, || {
|
|
state.set(#name {
|
|
#(#effect_fields,)*
|
|
..Default::default()
|
|
});
|
|
});
|
|
|
|
view! { cx,
|
|
Block(title=stringify!(#name).to_string()) {
|
|
#(#fields_view)*
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
syn::Data::Enum(e) => {
|
|
//dbg!(&e);
|
|
|
|
let variants = e.variants.iter().map(|v| {
|
|
let variant = v.ident.clone();
|
|
|
|
let inner = match &v.fields {
|
|
Fields::Unnamed(u) => {
|
|
u.unnamed.first().expect("the should be a field").ty.clone()
|
|
}
|
|
_ => unimplemented!(),
|
|
};
|
|
|
|
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
|
(variant_lower, variant, inner)
|
|
});
|
|
|
|
let first = variants.clone().next().unwrap().0;
|
|
|
|
let options = variants.clone().map(|(lower, variant, _inner)| {
|
|
let selected = first == lower;
|
|
quote! { option(value={stringify!(#lower)}, selected=#selected) { (stringify!(#variant)) } }
|
|
});
|
|
|
|
let view_match = variants.clone().map(|(lower, _variant, _inner)| {
|
|
let lower_str = format!("{}", lower);
|
|
|
|
quote! {
|
|
#lower_str => #lower.edit(cx)
|
|
}
|
|
});
|
|
|
|
let signals = variants.clone().map(|(lower, variant, inner)| {
|
|
quote! {
|
|
let #lower = if let #name::#variant(v) = state.get_untracked().as_ref().clone() {
|
|
create_signal(cx, v.clone())
|
|
} else {
|
|
create_signal(cx, <#inner>::default())
|
|
};
|
|
}
|
|
});
|
|
|
|
let effect_match = variants.clone().map(|(lower, variant, _inner)| {
|
|
let lower_str = format!("{}", lower);
|
|
|
|
quote! {
|
|
#lower_str => state.set(#name::#variant(#lower.get().as_ref().clone()))
|
|
}
|
|
});
|
|
|
|
quote! {
|
|
impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident {
|
|
fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View<G> {
|
|
let state = props.state;
|
|
|
|
let selected = create_signal(cx, String::from("0"));
|
|
|
|
#(#signals)*
|
|
|
|
create_effect(cx, || {
|
|
match selected.get().as_str() {
|
|
#(#effect_match,)*
|
|
_ => {}
|
|
//_ => unreachable!()
|
|
}
|
|
});
|
|
|
|
view! { cx,
|
|
Block(title=stringify!(#name).to_string()) {
|
|
select(bind:value=selected) {
|
|
#(#options)*
|
|
}
|
|
(match selected.get().as_str() {
|
|
#(#view_match,)*
|
|
_ => view! { cx, }
|
|
//_ => unreachable!()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => unimplemented!(),
|
|
};
|
|
|
|
println!("{}", &derived.to_string());
|
|
TokenStream::from(quote! {
|
|
#derived
|
|
|
|
#editable
|
|
})
|
|
}
|