yewstand 0.1.0

Zustand-inspired state management for Yew.
Documentation
//! Yewstand
//!
//! **Zustand-inspired state management for Yew** - Simple, declarative global stores using Rust macros.
//!
//! ## Features
//! - **Global Store**: Single source of truth with automatic React-like re-renders
//! - **Shallow Selectors**: Subscribe only to specific fields for optimal performance  
//! - **Mutation Macros**: Generate store-updating methods from simple functions
//!
//! ## Quick Start
//!
//! ```bash
//! cargo add yew --features="csr"
//! cargo add yewstand
//! ```
//!
//! ```rust,no_run
//! # #[macro_use] extern crate yewstand;
//! use yew::prelude::*;
//! use yewstand::{yewstand_store, yewstand_mutations};
//!
//! #[derive(Default, Clone, PartialEq)]
//! #[yewstand_store]  
//! pub struct AppStore {
//!     pub count: i32,
//! }
//!
//! #[yewstand_mutations]
//! impl AppStore {
//!     pub fn set_count(state: AppStore, value: i32) -> AppStore {
//!         AppStore { count: value, ..state }
//!     }
//! }
//!
//! // Usage in components
//! #[function_component(ExampleComponent)]
//! fn example_component() -> Html {
//!     let count = *use_app_store_shallow(|s| s.count);
//!     AppStore::set_count(42); // Updates global store
//!
//!     html! {
//!         <div>
//!             <p>{ format!("Count: {}", count) }</p>
//!         </div>
//!     }
//! }
//! # fn main() {}
//! ```

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};

/// Store macro - generates hooks and global store infrastructure.
///
/// # Examples
///
/// ```rust,no_run
/// use yewstand::yewstand_store;
///
/// #[derive(Default, Clone, PartialEq)]
/// #[yewstand_store]
/// pub struct AppStore {
///     pub count: i32,
/// }
/// # fn main() {}
/// ```
///
/// Generates `use_app_store()` and `use_app_store_shallow()` hooks.
#[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)
}

/// Mutation macro - transforms state functions into store updaters.
///
/// Functions must follow pattern: `fn mutation(state: AppStore, args...) -> AppStore`
/// **or** `async fn mutation(state: AppStore, args...) -> AppStore`
///
/// # Examples
///
/// ```rust,no_run
/// use yewstand::{yewstand_store, yewstand_mutations};
///
/// #[derive(Default, Clone, PartialEq)]
/// #[yewstand_store]
/// pub struct AppStore {
///     pub count: i32,
/// }
///
/// #[yewstand_mutations]
/// impl AppStore {
///     // Synchronous
///     pub fn set_count(state: AppStore, value: i32) -> AppStore {
///         AppStore { count: value, ..state }
///     }
///     
///     // Asynchronous ✅
///     pub async fn load_data(state: AppStore, id: String) -> AppStore {
///         // Simulate async work
///         AppStore { count: 99, ..state }
///     }
/// }
/// # fn main() {}
/// ```
#[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 = &param_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
            }
        }
    }
}