cheers-macros 0.1.0-alpha.1

Procedural macros for Cheers.
use std::collections::BTreeSet;

use proc_macro2::TokenStream;
use quote::quote;
use syn::{
    Error, Ident, ItemStruct, LitStr, Meta,
    parse::{Parse, ParseStream},
    parse2,
};

use crate::cheers::{IdField, filter_outer_attrs};

struct IdArgs {
    namespace: LitStr,
}

impl Parse for IdArgs {
    fn parse(input: ParseStream) -> Result<Self, Error> {
        Ok(Self {
            namespace: input.parse()?,
        })
    }
}

#[derive(Clone)]
struct DerivedIdSpec {
    method_ident: Ident,
    format_str: String,
}

fn method_ident_from_namespace(namespace: &LitStr) -> Result<Ident, Error> {
    let method_name = format!("id_{}", namespace.value());
    syn::parse_str::<Ident>(&method_name)
        .map_err(|_| Error::new_spanned(namespace, "id namespace must be a valid Rust identifier"))
}

fn instance_id_prefix_expr(base_id_format: &str, id_ident: Option<&Ident>) -> TokenStream {
    if let Some(id_ident) = id_ident {
        quote! { format!(#base_id_format, self.#id_ident) }
    } else {
        quote! { #base_id_format.to_owned() }
    }
}

pub(crate) fn generate_id_impls(
    item: &mut ItemStruct,
    struct_snake_case: &str,
    id_field: Option<IdField>,
) -> Result<TokenStream, Error> {
    let id_attrs = filter_outer_attrs(item, "id");

    let id_param = id_field
        .as_ref()
        .map(|IdField { ident, ty }| quote! { #ident: #ty });
    let id_ident = id_field.as_ref().map(|i| &i.ident);

    let mut derived_specs = Vec::new();
    let mut generated_method_names = BTreeSet::from([String::from("id")]);
    let mut generate_ids = id_field.is_some();
    let mut has_static_base_id_attr = false;

    for attr in &id_attrs {
        let args: IdArgs = match attr.meta.clone() {
            Meta::Path(_) => {
                if has_static_base_id_attr {
                    return Err(Error::new_spanned(
                        attr,
                        "only one struct-level #[id] attribute is allowed",
                    ));
                }
                has_static_base_id_attr = true;
                generate_ids = true;
                continue;
            }
            Meta::List(meta_list) => {
                generate_ids = true;
                parse2(meta_list.tokens)
            }
            _ => Err(Error::new_spanned(attr, "expected #[id] or #[id(...)]")),
        }?;

        let method_ident = method_ident_from_namespace(&args.namespace)?;
        if !generated_method_names.insert(method_ident.to_string()) {
            return Err(Error::new_spanned(
                &method_ident,
                "duplicate generated id method name",
            ));
        }

        let namespace_ending = format!("-{}", args.namespace.value());
        let format_str = format!("{{}}{namespace_ending}");

        derived_specs.push(DerivedIdSpec {
            method_ident,
            format_str,
        });
    }

    let vis = &item.vis;
    let struct_ident = &item.ident;

    let base_id_format = id_ident
        .map(|_| format!("{struct_snake_case}-{{}}"))
        .unwrap_or_else(|| struct_snake_case.to_owned());

    let hidden_id_prefix_method = {
        let id_prefix = instance_id_prefix_expr(&base_id_format, id_ident);

        quote! {
            #[doc(hidden)]
            /// Used by internal helper macros to access the current component instance id.
            /// Not part of the stable public API.
            #vis fn __id_prefix(&self) -> ::std::string::String {
                #id_prefix
            }
        }
    };

    if !generate_ids {
        let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
        return Ok(quote! {
            impl #impl_generics #struct_ident #ty_generics #where_clause {
                #hidden_id_prefix_method
            }
        });
    }

    let ids_ident = Ident::new(&format!("{}Ids", item.ident), item.ident.span());

    let mut derived_methods = Vec::new();
    let mut struct_fields = Vec::new();
    let mut method_fields = Vec::new();

    method_fields.push(quote! {
        id: ::cheers::prelude::ElementId::__dynamic(__id_prefix.clone())
    });

    for spec in derived_specs.iter() {
        let method_ident = &spec.method_ident;
        let format_str = &spec.format_str;

        derived_methods.push(quote! {
            #vis fn #method_ident(#id_param) -> ::cheers::prelude::ElementId {
                ::cheers::prelude::ElementId::__dynamic(format!(#format_str, Self::id(#id_ident)))
            }
        });

        struct_fields.push(quote! { #vis #method_ident: ::cheers::prelude::ElementId });
        method_fields.push(quote! { #method_ident: ::cheers::prelude::ElementId::__dynamic(format!(#format_str, __id_prefix)) });
    }

    let ids_struct = quote! {
        #vis struct #ids_ident {
            #vis id: ::cheers::prelude::ElementId,
            #(#struct_fields,)*
        }
    };

    let base_id_methods = {
        let dynamic_param = if let Some(id_ident) = id_ident {
            quote! { format!(#base_id_format, #id_ident) }
        } else {
            quote! { #base_id_format.to_owned() }
        };

        quote! {
            #vis fn id(#id_param) -> ::cheers::prelude::ElementId {
                ::cheers::prelude::ElementId::__dynamic(#dynamic_param)
            }
        }
    };

    let ids_accessor = {
        quote! {
            #hidden_id_prefix_method

            /// Returns the id bindings generated by `#[derive(Cheers)]`.
            #vis fn ids(&self) -> #ids_ident {
                let __id_prefix = self.__id_prefix();
                #ids_ident {
                    #(#method_fields,)*
                }
            }
        }
    };

    let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
    let methods_impl = quote! {
        impl #impl_generics #struct_ident #ty_generics #where_clause {
            #base_id_methods
            #(#derived_methods)*
            #ids_accessor
        }

    };

    Ok(quote! {
        #ids_struct
        #methods_impl
    })
}