hesione_macros 0.1.1

Macros for the hesione crate
Documentation
extern crate proc_macro;

use proc_macro2::TokenStream;
use quote::quote;
use syn::{
    braced,
    parse::{Parse, ParseStream, Result},
    parse_macro_input,
    punctuated::Punctuated,
    token, Attribute, Field, Ident, Lit, Meta, MetaNameValue, Token,
    Visibility,
};

/// Ensures derive input is a struct with named fields
struct NamedFieldsStruct {
    // Not read but used for parsing
    #[allow(dead_code)]
    attrs: Vec<Attribute>,

    // Not read but used for parsing
    #[allow(dead_code)]
    vis: Visibility,

    // Not read but used for parsing
    #[allow(dead_code)]
    struct_token: Token![struct],

    ident: Ident,

    // Not read but used for parsing
    #[allow(dead_code)]
    brace_token: token::Brace,

    fields: Punctuated<Field, Token![,]>,
}

impl Parse for NamedFieldsStruct {
    fn parse(input: ParseStream) -> Result<Self> {
        let content;

        // clippy doesn't like it but according to syn's docs, this is the
        // idiomatic form
        #[allow(clippy::eval_order_dependence)]
        Ok(Self {
            attrs: input.call(Attribute::parse_outer)?,
            vis: input.parse()?,
            struct_token: input.parse()?,
            ident: input.parse()?,
            brace_token: braced!(content in input),
            fields: content.parse_terminated(Field::parse_named)?,
        })
    }
}

/// Generate the calls to `new` for each metric
fn gen_metric_builder(field: &Field) -> TokenStream {
    let ty = &field.ty;
    let name = field
        .ident
        .as_ref()
        .expect("these should be guaranteed to be named already");

    let name_str = name.to_string();

    let help = field
        .attrs
        .iter()

        // Only interested in doc comments/attributes
        .filter(|attr| attr.path.is_ident("doc"))

        // Parse out standard attribute metadata, discarding failures
        .filter_map(|attr| attr.parse_meta().ok())

        // Get the string literal containing the docs, discarding weird cases
        .filter_map(|meta| {
            if let Meta::NameValue(MetaNameValue {
                lit: Lit::Str(s),
                ..
            }) = meta
            { Some(s) } else { None }
        })

        // For some reason, /// style doc comments appear to have a leading
        // whitespace; this gets rid of that
        .map(|help| help.value().trim_start().to_owned())

        // Grab the first successful doc comment (if there is one)
        .next()
        .map_or_else(
            // If there are no doc comments, don't call any extra functions
            TokenStream::new,

            // If there are doc comments, set the help text of the metric
            |help| quote! {
                .help(#help)
            },
        );

    quote! {
        #name: <#ty>::new(#name_str)
            .unwrap()
            #help,
    }
}

/// Generate the calls to `to_prometheus_lines` for each metric
fn gen_output_generator(field: &Field) -> TokenStream {
    let ty = &field.ty;
    let name = field
        .ident
        .as_ref()
        .expect("these should be guaranteed to be named already");

    // `buf` and `order` come from the scope defining the `Metrics` impl
    quote! {
        <#ty as ::hesione::Metric<_>>::
            to_prometheus_lines(&self.#name, &mut buf, order);
    }
}

/// Automatically implement [`Hesione`](hesione_shared::Hesione)
#[proc_macro_derive(Hesione)]
pub fn main(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let named_fields_struct = parse_macro_input!(input as NamedFieldsStruct);

    let struct_name = named_fields_struct.ident;

    let metric_builders: TokenStream = named_fields_struct
        .fields
        .iter()
        .flat_map(gen_metric_builder)
        .collect();

    let output_generators: TokenStream = named_fields_struct
        .fields
        .iter()
        .flat_map(gen_output_generator)
        .collect();

    let default_impl = quote! {
        impl Default for #struct_name {
            fn default() -> Self {
                Self {
                    #metric_builders
                }
            }
        }
    };

    let metrics_impl = quote! {
        impl ::hesione::Hesione for #struct_name {
            fn to_prometheus_lines(
                &self,
                order: ::core::sync::atomic::Ordering,
                capacity: Option<usize>
            ) -> String {
                let mut buf = String::with_capacity(capacity.unwrap_or(2048));

                #output_generators

                buf
            }
        }
    };

    let tokens = quote! {
        #metrics_impl
        #default_impl
    };

    tokens.into()
}