use heck::ToSnakeCase;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::ToTokens;
use quote::quote;
use syn::FnArg;
use syn::{DeriveInput, ImplItem, ImplItemFn, ItemImpl, parse_macro_input};
#[proc_macro_attribute]
pub fn yewstand_store(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let name = &input.ident;
let snake_name = name.to_string().to_snake_case();
let mod_name = Ident::new(&format!("{}_store", snake_name), Span::call_site());
let use_store_fn = Ident::new(&format!("use_{}", snake_name), Span::call_site());
let use_shallow_fn = Ident::new(&format!("use_{}_shallow", snake_name), Span::call_site());
let expanded = quote! {
#input
mod #mod_name {
use super::*;
use yew::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::collections::HashMap;
thread_local! {
static STORE: RefCell<Rc<RefCell<#name>>> = RefCell::new(Rc::new(RefCell::new(#name::default())));
static SUBSCRIBERS: RefCell<HashMap<u64, yew::Callback<()>>> = RefCell::new(HashMap::new());
static NEXT_SUB_ID: AtomicU64 = AtomicU64::new(0);
}
pub fn get_store() -> Rc<RefCell<#name>> {
STORE.with(|cell| cell.borrow().clone())
}
pub fn subscribe(callback: yew::Callback<()>) -> u64 {
let id = NEXT_SUB_ID.with(|atomic| {
atomic.fetch_add(1, Ordering::Relaxed)
});
SUBSCRIBERS.with(|subs| {
subs.borrow_mut().insert(id, callback);
});
id
}
pub fn unsubscribe(id: u64) {
SUBSCRIBERS.with(|subs| {
subs.borrow_mut().remove(&id);
});
}
pub fn notify_subscribers() {
SUBSCRIBERS.with(|subs| {
let callbacks: Vec<_> = subs.borrow()
.values()
.cloned()
.collect();
for callback in callbacks {
callback.emit(());
}
});
}
pub fn update_store<F>(updater: F)
where
F: FnOnce(&mut #name) + 'static,
{
let store = get_store();
let mut store_borrow = store.borrow_mut();
updater(&mut *store_borrow);
drop(store_borrow);
notify_subscribers();
}
pub fn set_store(new_state: #name) {
let store = get_store();
let mut store_borrow = store.borrow_mut();
*store_borrow = new_state;
drop(store_borrow);
notify_subscribers();
}
#[yew::hook]
pub fn #use_store_fn() -> yew::UseStateHandle<#name>
where
#name: Clone + PartialEq + 'static,
{
let state = use_state(|| #name::default());
{
let state = state.clone();
use_effect_with((), move |_| {
let state = state.clone();
let callback = Callback::from(move |_| {
let store = get_store();
let new_value = store.borrow().clone();
if *state != new_value {
state.set(new_value);
}
});
let id = subscribe(callback.clone());
move || {
unsubscribe(id);
}
});
}
state
}
#[yew::hook]
pub fn #use_shallow_fn<F, T>(selector: F) -> yew::UseStateHandle<T>
where
F: Fn(&#name) -> T + Clone + 'static,
T: Clone + PartialEq + 'static,
{
let state = use_state(|| selector(&<#name>::default()));
{
let state = state.clone();
let selector = selector.clone();
use_effect_with(state.clone(), move |current_state| {
let current_state = current_state.clone();
let selector = selector.clone();
let callback = Callback::from(move |_| {
let store = get_store();
let store_value = store.borrow().clone();
let new_value = selector(&store_value);
if *current_state != new_value {
state.set(new_value);
}
});
let id = subscribe(callback.clone());
move || {
unsubscribe(id);
}
});
}
state
}
}
pub use #mod_name::#use_store_fn;
pub use #mod_name::#use_shallow_fn;
pub use #mod_name::update_store;
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn yewstand_mutations(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemImpl);
let target_ty = input.self_ty.clone();
let original_methods: Vec<_> = input
.items
.iter()
.filter_map(|item| {
if let ImplItem::Fn(method) = item {
Some(method.clone())
} else {
None
}
})
.collect();
let processed_methods: Vec<_> = original_methods
.iter()
.map(|method| expand_updater_fn(&target_ty, method))
.collect();
let expanded = quote! {
impl #target_ty {
#(#processed_methods)*
}
};
expanded.into()
}
fn expand_updater_fn(target_ty: &syn::Type, method: &ImplItemFn) -> proc_macro2::TokenStream {
let snake_ty = target_ty.to_token_stream().to_string().to_snake_case();
let store_mod = Ident::new(&format!("{}_store", snake_ty), Span::call_site());
let method_name = &method.sig.ident;
let param_idents: Vec<_> = method
.sig
.inputs
.iter()
.filter_map(|arg| {
if let FnArg::Typed(pat_type) = arg {
Some(pat_type.pat.as_ref().clone())
} else {
None
}
})
.collect();
let param_types: Vec<_> = method
.sig
.inputs
.iter()
.filter_map(|arg| {
if let FnArg::Typed(pat_type) = arg {
Some(pat_type.ty.as_ref().clone())
} else {
None
}
})
.collect();
let user_param_idents: Vec<_> = param_idents.iter().skip(1).cloned().collect();
let user_param_types: Vec<_> = param_types.iter().skip(1).cloned().collect();
let state_param = ¶m_idents[0];
let original_body = &method.block;
let original_ret_ty = &method.sig.output;
quote! {
pub fn #method_name(#(#user_param_idents: #user_param_types),*) #original_ret_ty {
{
let current_state = #store_mod::get_store().borrow().clone();
let #state_param = current_state;
let new_state = #original_body;
#store_mod::set_store(new_state.clone());
new_state
}
}
}
}