aip-193-derive 0.1.2

A Rust implementation of Google AIP-193 Errors derive macros.
Documentation
use darling::{ast, FromDeriveInput, FromField, FromVariant};
use proc_macro::TokenStream;
use proc_macro_crate::{crate_name, FoundCrate};
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Ident};

#[derive(Debug, FromDeriveInput)]
#[darling(attributes(status), supports(enum_any))]
struct StatusInput {
    ident: Ident,
    data: ast::Data<StatusVariant, ()>,
    domain: String,
    #[darling(default)]
    into_response: bool,
}

#[derive(Debug, FromVariant)]
#[darling(attributes(status))]
struct StatusVariant {
    ident: Ident,
    fields: ast::Fields<StatusField>,
    code: Ident,
    #[darling(default)]
    message: Option<String>,
}

#[derive(Debug, FromField)]
#[darling(attributes(status))]
struct StatusField {
    ident: Option<Ident>,
    #[darling(default)]
    metadata: bool,
    #[darling(default)]
    metadata_key: Option<String>,
}

fn get_crate_path() -> proc_macro2::TokenStream {
    if let Ok(found) = crate_name("aip") {
        return match found {
            FoundCrate::Itself => quote!(crate::__private::errors),
            FoundCrate::Name(name) => {
                let ident = Ident::new(&name, proc_macro2::Span::call_site());
                quote!(::#ident::__private::errors)
            }
        };
    }
    
    if let Ok(found) = crate_name("aip-193") {
        return match found {
            FoundCrate::Itself => quote!(crate),
            FoundCrate::Name(name) => {
                let ident = Ident::new(&name, proc_macro2::Span::call_site());
                quote!(::#ident)
            }
        };
    }
    
    quote!(::aip_193)
}

#[proc_macro_derive(IntoStatus, attributes(status))]
pub fn derive_into_status(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    
    let parsed = match StatusInput::from_derive_input(&input) {
        Ok(v) => v,
        Err(e) => return e.write_errors().into(),
    };

    let expanded = generate_impl(&parsed);
    TokenStream::from(expanded)
}

fn generate_impl(input: &StatusInput) -> proc_macro2::TokenStream {
    let name = &input.ident;
    let domain = &input.domain;
    let krate = get_crate_path();

    let variants = match &input.data {
        ast::Data::Enum(variants) => variants,
        _ => panic!("IntoStatus only supports enums"),
    };

    let code_arms = generate_code_arms(name, variants, &krate);
    let message_arms = generate_message_arms(name, variants);
    let metadata_arms = generate_metadata_arms(name, variants);

    let into_status_impl = quote! {
        impl #krate::IntoStatus for #name {
            fn code(&self) -> #krate::Code {
                match self {
                    #(#code_arms),*
                }
            }

            fn message(&self) -> ::std::string::String {
                match self {
                    #(#message_arms),*
                }
            }

            fn reason(&self) -> &str {
                self.as_ref()
            }

            fn domain(&self) -> &str {
                #domain
            }

            fn metadata(&self) -> ::std::collections::HashMap<::std::string::String, ::std::string::String> {
                match self {
                    #(#metadata_arms),*
                }
            }
        }
    };

    let into_response_impl = if input.into_response {
        generate_into_response_impl(name, &krate)
    } else {
        quote! {}
    };

    quote! {
        #into_status_impl
        #into_response_impl
    }
}

fn get_axum_path() -> proc_macro2::TokenStream {
    if let Ok(found) = crate_name("axum") {
        return match found {
            FoundCrate::Itself => quote!(crate),
            FoundCrate::Name(name) => {
                let ident = Ident::new(&name, proc_macro2::Span::call_site());
                quote!(::#ident)
            }
        };
    }
    
    if let Ok(found) = crate_name("axum-core") {
        return match found {
            FoundCrate::Itself => quote!(crate),
            FoundCrate::Name(name) => {
                let ident = Ident::new(&name, proc_macro2::Span::call_site());
                quote!(::#ident)
            }
        };
    }
    
    quote!(::axum)
}

fn generate_into_response_impl(
    name: &Ident,
    krate: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
    let axum = get_axum_path();
    
    quote! {
        impl #axum::response::IntoResponse for #name {
            fn into_response(self) -> #axum::response::Response {
                let status: #krate::Status = #krate::IntoStatus::into_status(self);
                #axum::response::IntoResponse::into_response(status)
            }
        }
    }
}


fn generate_code_arms(
    enum_name: &Ident, 
    variants: &[StatusVariant],
    krate: &proc_macro2::TokenStream,
) -> Vec<proc_macro2::TokenStream> {
    variants.iter().map(|v| {
        let code = &v.code;
        let pattern = generate_pattern(enum_name, v);
        quote! {
            #pattern => #krate::Code::#code
        }
    }).collect()
}

fn generate_message_arms(
    enum_name: &Ident, 
    variants: &[StatusVariant],
) -> Vec<proc_macro2::TokenStream> {
    variants.iter().map(|v| {
        let variant_name = &v.ident;
        let pattern = generate_pattern(enum_name, v);
        
        let message_expr = if let Some(template) = &v.message {
            parse_message_template(template, &v.fields)
        } else {
            let default_msg = format!("{}", variant_name);
            quote! { #default_msg.to_string() }
        };
        
        quote! {
            #pattern => #message_expr
        }
    }).collect()
}

fn parse_message_template(
    template: &str,
    fields: &ast::Fields<StatusField>,
) -> proc_macro2::TokenStream {
    let field_names: Vec<String> = fields.iter()
        .filter_map(|f| f.ident.as_ref().map(|i| i.to_string()))
        .collect();
    
    let mut format_str = String::new();
    let mut args: Vec<proc_macro2::TokenStream> = Vec::new();
    
    let mut chars = template.chars().peekable();
    while let Some(c) = chars.next() {
        if c == '{' {
            let mut field_name = String::new();
            while let Some(&next) = chars.peek() {
                if next == '}' {
                    chars.next();
                    break;
                }
                field_name.push(chars.next().unwrap());
            }
            
            if field_names.contains(&field_name) {
                format_str.push_str("{}");
                let field_ident = Ident::new(&field_name, proc_macro2::Span::call_site());
                args.push(quote! { #field_ident });
            } else {
                format_str.push('{');
                format_str.push_str(&field_name);
                format_str.push('}');
            }
        } else {
            format_str.push(c);
        }
    }
    
    if args.is_empty() {
        quote! { #template.to_string() }
    } else {
        quote! { format!(#format_str, #(#args),*) }
    }
}

fn generate_metadata_arms(
    enum_name: &Ident, 
    variants: &[StatusVariant],
) -> Vec<proc_macro2::TokenStream> {
    variants.iter().map(|v| {
        let pattern = generate_pattern(enum_name, v);
        
        let metadata_fields: Vec<_> = v.fields.iter()
            .filter(|f| f.metadata)
            .filter_map(|f| {
                let field_name = f.ident.as_ref()?;
                let key = f.metadata_key.clone()
                    .unwrap_or_else(|| field_name.to_string());
                Some(quote! {
                    map.insert(#key.to_string(), #field_name.to_string());
                })
            })
            .collect();

        quote! {
            #pattern => {
                #[allow(unused_mut)]
                let mut map = ::std::collections::HashMap::new();
                #(#metadata_fields)*
                map
            }
        }
    }).collect()
}

fn generate_pattern(enum_name: &Ident, variant: &StatusVariant) -> proc_macro2::TokenStream {
    let variant_name = &variant.ident;
    
    match &variant.fields.style {
        ast::Style::Unit => {
            quote! { #enum_name::#variant_name }
        }
        ast::Style::Struct => {
            let field_names: Vec<_> = variant.fields.iter()
                .filter_map(|f| f.ident.as_ref())
                .collect();
            quote! { #enum_name::#variant_name { #(#field_names),* } }
        }
        ast::Style::Tuple => {
            let bindings: Vec<_> = (0..variant.fields.len())
                .map(|i| {
                    let ident = Ident::new(&format!("_{}", i), proc_macro2::Span::call_site());
                    quote! { #ident }
                })
                .collect();
            quote! { #enum_name::#variant_name(#(#bindings),*) }
        }
    }
}