amethystate-macros 0.9.1

Type-safe reactive persistence for Rust GUI apps
Documentation
use crate::amethystate::MacroArgs;
use crate::amethystate::generate::parse_default;
use amethystate_macros_core::StoreFieldEntry;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Ident, Visibility};

pub fn generate_wasm_code(
    crate_name: TokenStream2,
    vis: &Visibility,
    name: &Ident,
    attrs: &[syn::Attribute],
    prefix: Option<String>,
    entries: &[StoreFieldEntry],
    _macro_args: MacroArgs,
) -> TokenStream2 {
    let is_root = prefix.is_some();
    let prefix_str = prefix.unwrap_or_default();

    let backend_ty = quote! { ::amethystate::tauri::TauriBackend };

    let struct_fields = entries.iter().map(|e| {
        let fname = e.ident.as_ref().unwrap();
        let fvis = &e.vis;
        let ty = &e.ty;

        if e.nested || e.lookup_node.is_some() {
            let nested_type = get_type_ident(ty);
            quote! { #fvis #fname: #nested_type }
        } else if let Some((k, v)) = e.get_map_types() {
            quote! { #fvis #fname: #crate_name::client::ReactiveMap<#k, #v, #backend_ty> }
        } else {
            quote! { #fvis #fname: #crate_name::client::Field<#ty, #backend_ty> }
        }
    });

    let methods = entries.iter().map(|e| {
        let fname = e.ident.as_ref().unwrap();
        let ty = &e.ty;

        if e.nested || e.lookup_node.is_some() {
            let nested_type = get_type_ident(ty);
            quote! { pub fn #fname(&self) -> #nested_type { self.#fname.clone() } }
        } else if let Some((k, v)) = e.get_map_types() {
            quote! { pub fn #fname(&self) -> #crate_name::client::ReactiveMap<#k, #v, #backend_ty> { self.#fname.clone() } }
        } else {
            quote! { pub fn #fname(&self) -> #crate_name::client::Field<#ty, #backend_ty> { self.#fname.clone() } }
        }
    });

    let init_fields = entries.iter().map(|e| {
        let fname = e.ident.as_ref().unwrap();
        let key_suffix = if let Some(lookup) = &e.lookup { lookup.to_string() }
        else if let Some(lookup_node) = &e.lookup_node { lookup_node.to_string() }
        else { e.key.as_deref().unwrap_or(&fname.to_string()).to_string() };

        let has_lookup = e.lookup.is_some() || e.lookup_node.is_some();

        let full_key = if has_lookup || prefix_str == "." {
            key_suffix
        } else {
            format!("{prefix_str}.{key_suffix}")
        };


        let ty = &e.ty;
        let fallback = e.default.as_ref().map(parse_default).unwrap_or_else(|| quote! { ::std::default::Default::default() });

        if e.nested || e.lookup_node.is_some() {
            let nested_type = get_type_ident(ty);
            quote! { #fname: #nested_type::new_with_id(#full_key, &initial, store, instance_id) }
        } else if let Some((k, v)) = e.get_map_types() {
            quote! {
                #fname: {
                    let mut map_init = ::std::collections::HashMap::new();
                    let map_prefix = if #prefix_str == "." { format!("{}.", #full_key) } else { format!("{}.{}.", #prefix_str, #full_key) };
                    for (k, v) in initial {
                        if let Some(sub_key) = k.strip_prefix(&map_prefix) {
                            if let Ok(parsed_k) = <#k as ::std::str::FromStr>::from_str(sub_key) {
                                if let Ok(parsed_v) = store.decode::<#v>(v) {
                                    map_init.insert(parsed_k, parsed_v);
                                }
                            }
                        }
                    }
                    #crate_name::client::ReactiveMap::new_with_backend_and_id(#full_key, map_init, store.clone(), instance_id)
                }
            }
        } else {
            quote! {
                #fname: {
                    let val = initial.get(#full_key)
                        .and_then(|v| store.decode::<#ty>(v).ok())
                        .unwrap_or_else(|| #fallback);
                    #crate_name::client::Field::new_with_backend_and_id(#full_key, val, store.clone(), instance_id)
                }
            }
        }
    });

    let load_impl = if is_root {
        quote! {
            impl #crate_name::client::AmeStateSliceAsync<#backend_ty> for #name {
                type Error = <#backend_ty as #crate_name::client::AmeBackendAsync>::Error;

                async fn load_async(store: &#backend_ty) -> ::std::result::Result<Self, Self::Error> {
                    use #crate_name::client::AmeBackendAsync;
                    let scan_prefix = if #prefix_str == "." { "" } else { #prefix_str };
                    let raw_entries = store.scan_prefix(scan_prefix).await?;
                    let mut initial = ::std::collections::HashMap::new();
                    for (k, v) in raw_entries {
                        initial.insert(k, v);
                    }

                    Ok(Self::new_with_id(&initial, store, #crate_name::uuid::Uuid::new_v4()))
                }
            }

            impl #name {
                pub fn new_with_id(initial: &::std::collections::HashMap<String, <#backend_ty as #crate_name::client::AmeBackendAsync>::Raw>, store: &#backend_ty, instance_id: #crate_name::uuid::Uuid) -> Self {
                    use #crate_name::client::AmeBackendAsync;
                    Self {
                        __amethystate_instance_id: instance_id,
                        #(#init_fields,)*
                    }
                }
            }
        }
    } else {
        let nested_init_fields = entries.iter().map(|e| {
            let fname = e.ident.as_ref().unwrap();
            let key_str = e.key.as_deref().unwrap_or(&fname.to_string()).to_string();
            let ty = &e.ty;
            let fallback = e.default.as_ref().map(parse_default).unwrap_or_else(|| quote! { ::std::default::Default::default() });

            if e.nested || e.lookup_node.is_some() {
                let nested_type = get_type_ident(ty);
                quote! { #fname: #nested_type::new_with_id(&format!("{}.{}", prefix, #key_str), initial, store, instance_id) }
            } else if let Some((k, v)) = e.get_map_types() {
                quote! {
                    #fname: {
                        let mut map_init = ::std::collections::HashMap::new();
                        let map_prefix = if prefix == "." { format!("{}.", #key_str) } else { format!("{}.{}.", prefix, #key_str) };
                        for (k, v) in initial {
                            if let Some(sub_key) = k.strip_prefix(&map_prefix) {
                                if let Ok(parsed_k) = <#k as ::std::str::FromStr>::from_str(sub_key) {
                                    if let Ok(parsed_v) = store.decode::<#v>(v) {
                                        map_init.insert(parsed_k, parsed_v);
                                    }
                                }
                            }
                        }
                        let map_key = if prefix == "." { #key_str.to_string() } else { format!("{}.{}", prefix, #key_str) };
                        #crate_name::client::ReactiveMap::new_with_backend_and_id(map_key, map_init, store.clone(), instance_id)
                    }
                }
            } else {
                quote! {
                    #fname: {
                        let full_key = if prefix == "." { #key_str.to_string() } else { format!("{}.{}", prefix, #key_str) };
                        let val = initial.get(&full_key)
                            .and_then(|v| store.decode::<#ty>(v).ok())
                            .unwrap_or_else(|| #fallback);
                        #crate_name::client::Field::new_with_backend_and_id(full_key, val, store.clone(), instance_id)
                    }
                }
            }
        });

        quote! {
            impl #name {
                pub fn new(prefix: &str, initial: &::std::collections::HashMap<String, <#backend_ty as #crate_name::client::AmeBackendAsync>::Raw>, store: &#backend_ty) -> Self {
                    Self::new_with_id(prefix, initial, store, #crate_name::uuid::Uuid::new_v4())
                }

                pub fn new_with_id(prefix: &str, initial: &::std::collections::HashMap<String, <#backend_ty as #crate_name::client::AmeBackendAsync>::Raw>, store: &#backend_ty, instance_id: #crate_name::uuid::Uuid) -> Self {
                    use #crate_name::client::AmeBackendAsync;
                    Self {
                        __amethystate_instance_id: instance_id,
                        #(#nested_init_fields,)*
                    }
                }
            }
        }
    };

    let fork_fields = entries.iter().map(|e| {
        let fname = e.ident.as_ref().unwrap();
        quote! { #fname: self.#fname.fork_with_id(new_id) }
    });

    quote! {
        #[derive(Clone, Debug)]
        #(#attrs)* #vis struct #name {
            __amethystate_instance_id: #crate_name::uuid::Uuid,
            #(#struct_fields,)*
        }

        #load_impl

        impl #name {
            #(#methods)*

            pub fn fork(&self) -> Self {
                self.fork_with_id(#crate_name::uuid::Uuid::new_v4())
            }

            #[doc(hidden)]
            pub fn fork_with_id(&self, new_id: #crate_name::uuid::Uuid) -> Self {
                Self {
                    __amethystate_instance_id: new_id,
                    #(#fork_fields,)*
                }
            }
        }
    }
}

fn get_type_ident(ty: &syn::Type) -> proc_macro2::TokenStream {
    if let syn::Type::Path(type_path) = ty
        && let Some(segment) = type_path.path.segments.last()
    {
        let ident = &segment.ident;
        return quote! { #ident };
    }
    quote! { #ty }
}