cheers-macros 0.1.0-alpha.1

Procedural macros for Cheers.
mod form;
mod id;
mod prop;
mod signal;

use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Error, Ident, ItemStruct, Meta, Type, spanned::Spanned};

use crate::cheers::{
    form::generate_form_impl, id::generate_id_impls, prop::generate_prop_impl,
    signal::generate_signal_impl,
};

fn to_snake_case(s: &str) -> String {
    let mut result = String::new();
    for (i, c) in s.chars().enumerate() {
        if c.is_ascii_uppercase() {
            if i > 0 {
                result.push('_');
            }
            result.push(c.to_ascii_lowercase());
        } else {
            result.push(c);
        }
    }
    result
}

fn filter_outer_attrs(item: &mut ItemStruct, name: &'static str) -> Vec<Attribute> {
    let (attrs, remaining) = std::mem::take(&mut item.attrs)
        .into_iter()
        .partition(|a| a.path().is_ident(name));
    item.attrs = remaining;
    attrs
}

#[derive(Clone)]
pub(crate) struct IdField {
    pub ident: Ident,
    pub ty: Type,
}

fn find_id_field(item: &ItemStruct) -> Result<Option<IdField>, Error> {
    let mut id_field = None;

    for field in &item.fields {
        let mut attrs = field.attrs.iter().filter(|a| a.path().is_ident("id"));
        let Some(attr) = attrs.next() else {
            continue;
        };

        if attrs.next().is_some() {
            return Err(Error::new_spanned(
                field,
                "only one #[id] attribute is allowed on a field",
            ));
        }

        match &attr.meta {
            Meta::Path(_) => {}
            _ => {
                return Err(Error::new_spanned(
                    attr,
                    "field #[id] does not accept arguments",
                ));
            }
        }

        if id_field.is_some() {
            return Err(Error::new_spanned(
                field,
                "only one field can be marked with #[id]",
            ));
        }

        let ident = field
            .ident
            .clone()
            .unwrap_or_else(|| Ident::new("id", field.span()));
        id_field = Some(IdField {
            ident,
            ty: field.ty.clone(),
        });
    }

    Ok(id_field)
}

fn to_owned_type(ty: &Type) -> Type {
    if let Type::Reference(ty_ref) = ty {
        let inner = &*ty_ref.elem;

        if let Type::Path(tp) = inner
            && tp.qself.is_none()
            && tp.path.is_ident("str")
        {
            return syn::parse_quote_spanned!(ty.span() => ::std::string::String);
        }

        inner.clone()
    } else {
        ty.clone()
    }
}

pub fn generate(mut item: ItemStruct) -> Result<TokenStream, Error> {
    let struct_snake_case = to_snake_case(&item.ident.to_string());
    let id_field = find_id_field(&item)?;

    let id_impl = generate_id_impls(&mut item, &struct_snake_case, id_field.clone())?;
    let form_impl = generate_form_impl(&mut item)?;
    let prop_impl = generate_prop_impl(&mut item)?;
    let signal_impl = generate_signal_impl(item, struct_snake_case, id_field)?;

    Ok(quote! {
        #id_impl

        #signal_impl

        #form_impl

        #prop_impl
    })
}