iroh_metrics_derive/
lib.rsuse heck::ToSnakeCase;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
    meta::ParseNestedMeta, parse_macro_input, spanned::Spanned, Attribute, Data, DeriveInput,
    Error, Expr, ExprLit, Fields, Ident, Lit, LitStr,
};
#[proc_macro_derive(MetricsGroup, attributes(metrics, default))]
pub fn derive_metrics_group(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let mut out = proc_macro2::TokenStream::new();
    out.extend(expand_metrics(&input).unwrap_or_else(Error::into_compile_error));
    out.extend(expand_iterable(&input).unwrap_or_else(Error::into_compile_error));
    out.into()
}
#[proc_macro_derive(Iterable)]
pub fn derive_iterable(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let out = expand_iterable(&input).unwrap_or_else(Error::into_compile_error);
    out.into()
}
#[proc_macro_derive(MetricsGroupSet, attributes(metrics))]
pub fn derive_metrics_group_set(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let mut out = proc_macro2::TokenStream::new();
    out.extend(expand_metrics_group_set(&input).unwrap_or_else(Error::into_compile_error));
    out.into()
}
fn expand_iterable(input: &DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
    let (name, fields) = parse_named_struct(input)?;
    let count = fields.len();
    let mut match_arms = quote! {};
    for (i, field) in fields.iter().enumerate() {
        let ident = field.ident.as_ref().unwrap();
        let ident_str = ident.to_string();
        let attr = parse_metrics_attr(&field.attrs)?;
        let help = attr
            .help
            .or_else(|| parse_doc_first_line(&field.attrs))
            .unwrap_or_else(|| ident_str.clone());
        match_arms.extend(quote! {
            #i => Some(::iroh_metrics::MetricItem::new(#ident_str, #help, &self.#ident as &dyn ::iroh_metrics::Metric)),
        });
    }
    Ok(quote! {
        impl ::iroh_metrics::iterable::Iterable for #name {
            fn field_count(&self) -> usize {
                #count
            }
            fn field_ref(&self, n: usize) -> Option<::iroh_metrics::MetricItem<'_>> {
                match n {
                    #match_arms
                    _ => None,
                }
            }
        }
    })
}
fn expand_metrics(input: &DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
    let (name, fields) = parse_named_struct(input)?;
    let attr = parse_metrics_attr(&input.attrs)?;
    let name_str = attr
        .name
        .unwrap_or_else(|| name.to_string().to_snake_case());
    let default = if attr.default {
        let mut items = vec![];
        for field in fields.iter() {
            let ident = field
                .ident
                .as_ref()
                .ok_or_else(|| Error::new(field.span(), "Only named fields are supported"))?;
            let attr = parse_default_attr(&field.attrs)?;
            let item = if let Some(expr) = attr {
                quote!( #ident: #expr)
            } else {
                quote!( #ident: ::std::default::Default::default() )
            };
            items.push(item);
        }
        Some(quote! {
            impl ::std::default::Default for #name {
                fn default() -> Self {
                    Self {
                        #(#items),*
                    }
                }
            }
        })
    } else {
        None
    };
    Ok(quote! {
        impl ::iroh_metrics::MetricsGroup for #name {
            fn name(&self) -> &'static str {
                #name_str
            }
        }
        #default
    })
}
fn expand_metrics_group_set(input: &DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
    let (name, fields) = parse_named_struct(input)?;
    let attr = parse_metrics_attr(&input.attrs)?;
    let name_str = attr
        .name
        .unwrap_or_else(|| name.to_string().to_snake_case());
    let mut cloned = quote! {};
    let mut refs = quote! {};
    for field in fields {
        let name = field.ident.as_ref().unwrap();
        cloned.extend(quote! {
            self.#name.clone() as ::std::sync::Arc<dyn ::iroh_metrics::MetricsGroup>,
        });
        refs.extend(quote! {
            &*self.#name as &dyn ::iroh_metrics::MetricsGroup,
        });
    }
    Ok(quote! {
        impl ::iroh_metrics::MetricsGroupSet for #name {
            fn name(&self) -> &'static str {
                #name_str
            }
            fn groups_cloned(&self) -> impl ::std::iter::Iterator<Item = ::std::sync::Arc<dyn ::iroh_metrics::MetricsGroup>> {
                [#cloned].into_iter()
            }
            fn groups(&self) -> impl ::std::iter::Iterator<Item = &dyn ::iroh_metrics::MetricsGroup> {
                [#refs].into_iter()
            }
        }
    })
}
fn parse_doc_first_line(attrs: &[Attribute]) -> Option<String> {
    attrs
        .iter()
        .filter(|attr| attr.path().is_ident("doc"))
        .flat_map(|attr| attr.meta.require_name_value())
        .find_map(|name_value| {
            let Expr::Lit(ExprLit { lit, .. }) = &name_value.value else {
                return None;
            };
            let Lit::Str(str) = lit else { return None };
            Some(str.value().trim().to_string())
        })
}
#[derive(Default)]
struct MetricsAttr {
    name: Option<String>,
    help: Option<String>,
    default: bool,
}
fn parse_default_attr(attrs: &[Attribute]) -> Result<Option<syn::Expr>, syn::Error> {
    let Some(attr) = attrs.iter().find(|attr| attr.path().is_ident("default")) else {
        return Ok(None);
    };
    let expr = attr.parse_args::<Expr>()?;
    Ok(Some(expr))
}
fn parse_metrics_attr(attrs: &[Attribute]) -> Result<MetricsAttr, syn::Error> {
    let mut out = MetricsAttr::default();
    for attr in attrs.iter().filter(|attr| attr.path().is_ident("metrics")) {
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("name") {
                out.name = Some(parse_lit_str(&meta)?);
                Ok(())
            } else if meta.path.is_ident("help") {
                out.help = Some(parse_lit_str(&meta)?);
                Ok(())
            } else if meta.path.is_ident("default") {
                out.default = true;
                Ok(())
            } else {
                Err(meta.error(
                    "The `metrics` attribute supports only `name`, `help` and `default` fields.",
                ))
            }
        })?;
    }
    Ok(out)
}
fn parse_lit_str(meta: &ParseNestedMeta<'_>) -> Result<String, Error> {
    let s: LitStr = meta.value()?.parse()?;
    Ok(s.value().trim().to_string())
}
fn parse_named_struct(input: &DeriveInput) -> Result<(&Ident, &Fields), Error> {
    match &input.data {
        Data::Struct(data) if matches!(data.fields, Fields::Named(_)) => {
            Ok((&input.ident, &data.fields))
        }
        _ => Err(Error::new(
            input.span(),
            "The `MetricsGroup` and `Iterable` derives support only structs.",
        )),
    }
}