tyzen-macro 0.1.10

Procedural macros for the tyzen crate.
Documentation
use quote::quote;
use syn::{Data, DeriveInput, Field, Fields};
use super::attr::{SerdeAttrs, has_tyzen_optional, option_inner_type, serde_attrs};
use super::case::{RenameRule, apply_rename_rule};

pub fn structure_definition(input: &DeriveInput, generic_params: &[String]) -> proc_macro2::TokenStream {
    let serde = serde_attrs(&input.attrs);
    match &input.data {
        Data::Struct(data) => {
            if serde.transparent {
                let Some(field) = data.fields.iter().next() else {
                    return syn::Error::new_spanned(
                        &input.ident,
                        "#[serde(transparent)] requires exactly one field",
                    )
                    .into_compile_error();
                };
                let ty_name = ts_type_name(&field.ty, generic_params);
                quote! {
                    || ::tyzen::meta::TypeStructure::Transparent(#ty_name)
                }
            } else {
                match &data.fields {
                    Fields::Named(_) => {
                        let fields = struct_fields_meta(&data.fields, serde.rename_all, generic_params);
                        quote! {
                            || ::tyzen::meta::TypeStructure::Struct(::tyzen::meta::StructMeta {
                                fields: &[#(#fields),*]
                            })
                        }
                    }
                    Fields::Unnamed(f) => {
                        let types = f.unnamed.iter().map(|field| {
                            let ty_name = ts_type_name(&field.ty, generic_params);
                            quote! { || #ty_name }
                        });
                        quote! {
                            || ::tyzen::meta::TypeStructure::Tuple(&[#(#types),*])
                        }
                    }
                    Fields::Unit => {
                        quote! { || ::tyzen::meta::TypeStructure::Unit }
                    }
                }
            }
        }
        Data::Enum(data) => {
            let variants = enum_variants_meta(&data.variants, &serde, generic_params);
            let tag = serde.tag.as_deref();
            let tag_quote = match tag {
                Some(t) => quote! { Some(#t) },
                None => quote! { None },
            };
            let content = serde.content.as_deref();
            let content_quote = match content {
                Some(c) => quote! { Some(#c) },
                None => quote! { None },
            };
            let untagged = serde.untagged;
            quote! {
                || ::tyzen::meta::TypeStructure::Enum(::tyzen::meta::EnumMeta {
                    variants: &[#(#variants),*],
                    tag: #tag_quote,
                    content: #content_quote,
                    untagged: #untagged,
                })
            }
        }
        _ => quote! { || ::tyzen::meta::TypeStructure::Unit },
    }
}

fn struct_fields_meta(
    fields: &Fields,
    rename_all: Option<RenameRule>,
    generic_params: &[String],
) -> Vec<proc_macro2::TokenStream> {
    fields
        .iter()
        .filter_map(|field| field_meta(field, rename_all, generic_params))
        .collect()
}

fn field_meta(field: &Field, rename_all: Option<RenameRule>, generic_params: &[String]) -> Option<proc_macro2::TokenStream> {
    let serde = serde_attrs(&field.attrs);
    if serde.skip {
        return None;
    }

    let field_name = field.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
    let renamed_name = ts_name(&field_name, serde.rename.clone(), rename_all);
    let optional = has_tyzen_optional(&field.attrs) || serde.default;
    let flattened = serde.flatten;
    
    let ty = if has_tyzen_optional(&field.attrs) {
        option_inner_type(&field.ty).unwrap()
    } else {
        &field.ty
    };

    let flatten_base_name = if flattened {
        let syn::Type::Path(p) = ty else {
            return Some(syn::Error::new_spanned(
                &field.ty,
                "#[serde(flatten)] field must be a named type path",
            )
            .into_compile_error());
        };
        let Some(last_seg) = p.path.segments.last() else {
            return Some(syn::Error::new_spanned(
                &field.ty,
                "#[serde(flatten)] field has an empty type path",
            )
            .into_compile_error());
        };
        let s = last_seg.ident.to_string();
        quote! { Some(#s) }
    } else {
        quote! { None }
    };

    let ty_name = ts_type_name(ty, generic_params);

    Some(quote! {
        ::tyzen::meta::FieldMeta {
            name: #renamed_name,
            ty_name: || #ty_name,
            optional: #optional,
            flattened: #flattened,
            flatten_base_name: #flatten_base_name,
        }
    })
}

fn enum_variants_meta(
    variants: &syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
    container_serde: &SerdeAttrs,
    generic_params: &[String],
) -> Vec<proc_macro2::TokenStream> {
    variants
        .iter()
        .filter_map(|variant| {
            let variant_serde = serde_attrs(&variant.attrs);
            if variant_serde.skip {
                return None;
            }

            let variant_name = ts_name(
                &variant.ident.to_string(),
                variant_serde.rename.clone(),
                container_serde.rename_all,
            );

            let fields = match &variant.fields {
                Fields::Unit => quote! { ::tyzen::meta::VariantFields::Unit },
                Fields::Unnamed(f) => {
                    let types = f.unnamed.iter().map(|field| {
                        let ty_name = ts_type_name(&field.ty, generic_params);
                        quote! { || #ty_name }
                    });
                    quote! { ::tyzen::meta::VariantFields::Unnamed(&[#(#types),*]) }
                }
                Fields::Named(_f) => {
                    let field_rename_all = variant_serde.rename_all.or(container_serde.rename_all_fields);
                    let fields = struct_fields_meta(&variant.fields, field_rename_all, generic_params);
                    quote! { ::tyzen::meta::VariantFields::Named(&[#(#fields),*]) }
                }
            };

            Some(quote! {
                ::tyzen::meta::VariantMeta {
                    name: #variant_name,
                    fields: #fields,
                }
            })
        })
        .collect()
}

fn ts_name(name: &str, rename: Option<String>, rename_all: Option<RenameRule>) -> String {
    if let Some(rename) = rename {
        rename
    } else if let Some(rule) = rename_all {
        apply_rename_rule(name, rule)
    } else {
        name.to_string()
    }
}

fn ts_type_name(ty: &syn::Type, generic_params: &[String]) -> proc_macro2::TokenStream {
    if let Some(inner) = channel_inner_type(ty) {
        let inner_ts = ts_type_name(inner, generic_params);
        return quote! { format!("Channel<{}>", #inner_ts) };
    }

    if let Some(name) = get_generic_aware_name(ty, generic_params) {
        quote! { #name }
    } else {
        quote! { <#ty as ::tyzen::TsType>::ts_name() }
    }
}

fn channel_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
    let syn::Type::Path(type_path) = ty else {
        return None;
    };

    let segment = type_path.path.segments.last()?;
    if segment.ident != "Channel" {
        return None;
    }

    let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
        return None;
    };

    args.args.iter().find_map(|arg| match arg {
        syn::GenericArgument::Type(inner) => Some(inner),
        _ => None,
    })
}

fn get_generic_aware_name(
    ty: &syn::Type,
    generic_params: &[String],
) -> Option<proc_macro2::TokenStream> {
    match ty {
        syn::Type::Path(p) => {
            let segment = p.path.segments.last()?;
            let ident_str = segment.ident.to_string();

            if generic_params.contains(&ident_str) {
                return Some(quote! { #ident_str.to_string() });
            }

            if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
                let mut has_generic = false;
                let inner_names = args
                    .args
                    .iter()
                    .filter_map(|arg| {
                        if let syn::GenericArgument::Type(inner_ty) = arg {
                            if is_generic(inner_ty, generic_params) {
                                has_generic = true;
                                return Some(ts_type_name(inner_ty, generic_params));
                            }
                            Some(quote! { <#inner_ty as ::tyzen::TsType>::ts_name() })
                        } else {
                            None
                        }
                    })
                    .collect::<Vec<_>>();

                if !has_generic || inner_names.is_empty() {
                    return None;
                }

                return match ident_str.as_str() {
                    "Vec" | "VecDeque" | "LinkedList" => {
                        let inner = &inner_names[0];
                        Some(quote! { format!("{}[]", #inner) })
                    }
                    "Option" => {
                        let inner = &inner_names[0];
                        Some(quote! { format!("{} | null", #inner) })
                    }
                    "Result" => {
                        match inner_names.len() {
                            1 => {
                                let ok = &inner_names[0];
                                Some(quote! { format!("Result<{}, string>", #ok) })
                            }
                            2 => {
                                let ok = &inner_names[0];
                                let err = &inner_names[1];
                                Some(quote! { format!("Result<{}, {}>", #ok, #err) })
                            }
                            _ => None,
                        }
                    }
                    _ => Some(
                        quote! { format!("{}<{}>", #ident_str, vec![#(#inner_names),*].join(", ")) },
                    ),
                };
            }
            None
        }
        _ => None,
    }
}

fn is_generic(ty: &syn::Type, generic_params: &[String]) -> bool {
    match ty {
        syn::Type::Path(p) => {
            if let Some(ident) = p.path.get_ident()
                && generic_params.contains(&ident.to_string())
            {
                return true;
            }
            p.path.segments.iter().any(|s| {
                if let syn::PathArguments::AngleBracketed(args) = &s.arguments {
                    args.args.iter().any(|arg| {
                        if let syn::GenericArgument::Type(inner) = arg {
                            is_generic(inner, generic_params)
                        } else {
                            false
                        }
                    })
                } else {
                    false
                }
            })
        }
        _ => false,
    }
}