tyzen-macro 0.2.3

Procedural macros for the tyzen crate.
Documentation
use super::case::{RenameRule, rename_rule};
use syn::{Attribute, GenericArgument, PathArguments, Type};

#[derive(Default)]
pub struct SerdeAttrs {
    pub rename: Option<String>,
    pub rename_all: Option<RenameRule>,
    pub rename_all_fields: Option<RenameRule>,
    pub tag: Option<String>,
    pub content: Option<String>,
    pub untagged: bool,
    pub transparent: bool,
    pub skip: bool,
    pub flatten: bool,
    pub default: bool,
    pub alias: Vec<String>,
    pub binary: bool,
}

pub fn serde_attrs(attrs: &[Attribute]) -> SerdeAttrs {
    let mut serde = SerdeAttrs::default();

    for attr in attrs {
        if !attr.path().is_ident("serde") {
            continue;
        }

        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("skip")
                || meta.path.is_ident("skip_serializing")
                || meta.path.is_ident("skip_deserializing")
            {
                serde.skip = true;
                return Ok(());
            }

            if meta.path.is_ident("rename") {
                let value = meta.value()?.parse::<syn::LitStr>()?;
                serde.rename = Some(value.value());
                return Ok(());
            }

            if meta.path.is_ident("rename_all") {
                let value = meta.value()?.parse::<syn::LitStr>()?;
                serde.rename_all = rename_rule(&value.value());
                return Ok(());
            }

            if meta.path.is_ident("rename_all_fields") {
                let value = meta.value()?.parse::<syn::LitStr>()?;
                serde.rename_all_fields = rename_rule(&value.value());
                return Ok(());
            }

            if meta.path.is_ident("tag") {
                let value = meta.value()?.parse::<syn::LitStr>()?;
                serde.tag = Some(value.value());
                return Ok(());
            }

            if meta.path.is_ident("content") {
                let value = meta.value()?.parse::<syn::LitStr>()?;
                serde.content = Some(value.value());
                return Ok(());
            }

            if meta.path.is_ident("untagged") {
                serde.untagged = true;
                return Ok(());
            }

            if meta.path.is_ident("transparent") {
                serde.transparent = true;
                return Ok(());
            }

            if meta.path.is_ident("flatten") {
                serde.flatten = true;
                return Ok(());
            }

            if meta.path.is_ident("default") {
                serde.default = true;
                return Ok(());
            }

            if meta.path.is_ident("alias") {
                let value = meta.value()?.parse::<syn::LitStr>()?;
                serde.alias.push(value.value());
                return Ok(());
            }

            if meta.path.is_ident("with") {
                let value = meta.value()?.parse::<syn::LitStr>()?;
                if value.value() == "serde_bytes" {
                    serde.binary = true;
                }
                return Ok(());
            }

            Ok(())
        });
    }

    serde
}

#[derive(Clone)]
pub enum VariantMetaValue {
    Lit(String),
    List(Vec<syn::Path>),
}

impl std::fmt::Debug for VariantMetaValue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Lit(s) => f.debug_tuple("Lit").field(s).finish(),
            Self::List(paths) => {
                let path_strs: Vec<String> = paths
                    .iter()
                    .map(|p| {
                        use quote::ToTokens;
                        p.to_token_stream().to_string().replace(" ", "")
                    })
                    .collect();
                f.debug_tuple("List").field(&path_strs).finish()
            }
        }
    }
}

#[derive(Default)]
pub struct TyzenAttrs {
    pub optional: bool,
    pub nullable: bool,
    pub meta_name: Option<String>,
    pub ns: Option<String>,
    pub apply: Option<syn::Path>,
    pub variant_meta: Vec<(String, VariantMetaValue)>,
    pub binary: bool,
    pub schema: bool,
}

pub fn tyzen_attrs(attrs: &[Attribute]) -> TyzenAttrs {
    let mut tyzen = TyzenAttrs::default();

    for attr in attrs {
        if !attr.path().is_ident("tyzen") {
            continue;
        }

        // Handle both #[tyzen(optional)] and #[tyzen(key = "val")]
        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("optional") {
                tyzen.optional = true;
                return Ok(());
            }

            if meta.path.is_ident("nullable") {
                tyzen.nullable = true;
                return Ok(());
            }

            if meta.path.is_ident("binary") {
                tyzen.binary = true;
                return Ok(());
            }

            if meta.path.is_ident("schema") {
                tyzen.schema = true;
                return Ok(());
            }

            if meta.path.is_ident("meta") {
                let value = meta.value()?.parse::<syn::LitStr>()?;
                tyzen.meta_name = Some(value.value());
                return Ok(());
            }

            if meta.path.is_ident("ns") || meta.path.is_ident("namespace") {
                let value = meta.value()?.parse::<syn::LitStr>()?;
                tyzen.ns = Some(value.value());
                return Ok(());
            }

            if meta.path.is_ident("apply") {
                let value = meta.value()?.parse::<syn::Path>()?;
                tyzen.apply = Some(value);
                return Ok(());
            }

            // Catch-all for variant metadata: #[tyzen(code = "404", msg = "Not Found")]
            if let Some(ident) = meta.path.get_ident() {
                let key = ident.to_string();
                if meta.input.peek(syn::token::Paren) {
                    let mut list = Vec::new();
                    let _ = meta.parse_nested_meta(|nested| {
                        list.push(nested.path.clone());
                        Ok(())
                    });
                    tyzen.variant_meta.push((key, VariantMetaValue::List(list)));
                    return Ok(());
                } else {
                    let value = meta.value()?.parse::<syn::LitStr>()?;
                    tyzen
                        .variant_meta
                        .push((key, VariantMetaValue::Lit(value.value())));
                    return Ok(());
                }
            }

            Ok(())
        });
    }

    tyzen
}

pub fn has_tyzen_optional(attrs: &[Attribute]) -> bool {
    tyzen_attrs(attrs).optional
}

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

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

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

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

pub fn parse_field_validation(attrs: &[syn::Attribute]) -> Option<proc_macro2::TokenStream> {
    let mut min_length = None;
    let mut max_length = None;
    let mut regex_pattern = None;
    let mut min_value = None;
    let mut max_value = None;
    let mut message = None;
    let mut has_any = false;

    for attr in attrs {
        if !attr.path().is_ident("validate") {
            continue;
        }

        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("length") {
                has_any = true;
                let _ = meta.parse_nested_meta(|nested| {
                    if nested.path.is_ident("min") {
                        let value = nested.value()?.parse::<syn::LitInt>()?;
                        min_length = Some(value.base10_parse::<usize>()?);
                    } else if nested.path.is_ident("max") {
                        let value = nested.value()?.parse::<syn::LitInt>()?;
                        max_length = Some(value.base10_parse::<usize>()?);
                    } else if nested.path.is_ident("message") {
                        let value = nested.value()?.parse::<syn::LitStr>()?;
                        message = Some(value.value());
                    }
                    Ok(())
                });
                return Ok(());
            }

            if meta.path.is_ident("regex") {
                has_any = true;
                if meta.input.peek(syn::token::Paren) {
                    let _ = meta.parse_nested_meta(|nested| {
                        if nested.path.is_ident("path") {
                            let value_expr = nested.value()?.parse::<syn::Expr>()?;
                            let mut pattern = None;
                            if let syn::Expr::Unary(syn::ExprUnary { expr, .. }) = &value_expr {
                                if let syn::Expr::Call(syn::ExprCall { func, .. }) = &**expr {
                                    if let syn::Expr::Path(syn::ExprPath { path, .. }) = &**func {
                                        if let Some(segment) = path.segments.last() {
                                            if segment.ident == "re_code" {
                                                pattern = Some("^[A-Z0-9]{2,5}$".to_string());
                                            }
                                        }
                                    }
                                }
                            }
                            if pattern.is_none() {
                                if let syn::Expr::Lit(syn::ExprLit {
                                    lit: syn::Lit::Str(s),
                                    ..
                                }) = value_expr
                                {
                                    pattern = Some(s.value());
                                }
                            }
                            regex_pattern = pattern;
                        } else if nested.path.is_ident("message") {
                            let value = nested.value()?.parse::<syn::LitStr>()?;
                            message = Some(value.value());
                        }
                        Ok(())
                    });
                } else {
                    let value = meta.value()?.parse::<syn::LitStr>()?;
                    regex_pattern = Some(value.value());
                }
                return Ok(());
            }

            if meta.path.is_ident("range") {
                has_any = true;
                let _ = meta.parse_nested_meta(|nested| {
                    if nested.path.is_ident("min") {
                        let val_lit = nested.value()?.parse::<syn::Lit>()?;
                        match val_lit {
                            syn::Lit::Float(f) => {
                                min_value = Some(f.base10_parse::<f64>()?);
                            }
                            syn::Lit::Int(i) => {
                                min_value = Some(i.base10_parse::<i64>()? as f64);
                            }
                            _ => {}
                        }
                    } else if nested.path.is_ident("max") {
                        let val_lit = nested.value()?.parse::<syn::Lit>()?;
                        match val_lit {
                            syn::Lit::Float(f) => {
                                max_value = Some(f.base10_parse::<f64>()?);
                            }
                            syn::Lit::Int(i) => {
                                max_value = Some(i.base10_parse::<i64>()? as f64);
                            }
                            _ => {}
                        }
                    } else if nested.path.is_ident("message") {
                        let value = nested.value()?.parse::<syn::LitStr>()?;
                        message = Some(value.value());
                    }
                    Ok(())
                });
                return Ok(());
            }

            Ok(())
        });
    }

    if has_any {
        let min_len_quote = match min_length {
            Some(l) => quote::quote! { Some(#l) },
            None => quote::quote! { None },
        };
        let max_len_quote = match max_length {
            Some(l) => quote::quote! { Some(#l) },
            None => quote::quote! { None },
        };
        let regex_quote = match regex_pattern {
            Some(ref r) => quote::quote! { Some(#r) },
            None => quote::quote! { None },
        };
        let min_val_quote = match min_value {
            Some(v) => quote::quote! { Some(#v) },
            None => quote::quote! { None },
        };
        let max_val_quote = match max_value {
            Some(v) => quote::quote! { Some(#v) },
            None => quote::quote! { None },
        };
        let msg_quote = match message {
            Some(ref m) => quote::quote! { Some(#m) },
            None => quote::quote! { None },
        };

        Some(quote::quote! {
            Some(::tyzen::meta::ValidationRule {
                min_length: #min_len_quote,
                max_length: #max_len_quote,
                regex_pattern: #regex_quote,
                min_value: #min_val_quote,
                max_value: #max_val_quote,
                message: #msg_quote,
            })
        })
    } else {
        None
    }
}