#![deny(missing_docs)]
#![doc = include_str!("../README.md")]
mod emf;
mod entry_impl;
use darling::{FromField, FromMeta, ast::NestedMeta, util::Flag};
use emf::{DimensionSets, NameStyle};
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as Ts2};
use quote::{ToTokens, format_ident, quote, quote_spanned};
use syn::{
Attribute, Data, DeriveInput, Error, Fields, Generics, Ident, Result, Type, Visibility,
parse_macro_input, spanned::Spanned,
};
#[proc_macro_attribute]
pub fn metrics(attr: TokenStream, input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let mut base_token_stream = Ts2::new();
let root_attrs = match parse_root_attrs(attr) {
Ok(root_attrs) => root_attrs,
Err(e) => {
e.to_compile_error().to_tokens(&mut base_token_stream);
RootAttributes::default()
}
};
match generate_metrics(root_attrs, input.clone()) {
Ok(output) => output.to_tokens(&mut base_token_stream),
Err(err) => {
clean_base_struct(&input).to_tokens(&mut base_token_stream);
err.to_compile_error().to_tokens(&mut base_token_stream);
}
};
base_token_stream.into()
}
#[derive(Debug, Default, FromMeta)]
struct RootAttributes {
prefix: Option<String>,
#[darling(default)]
rename_all: NameStyle,
#[darling(rename = "emf::dimension_sets")]
emf_dimensions: Option<DimensionSets>,
subfield: Flag,
}
impl RootAttributes {
fn configuration_field_names(&self) -> Vec<Ts2> {
if let Some(_dims) = &self.emf_dimensions {
vec![quote! { __config__ }]
} else {
vec![]
}
}
fn configuration_fields(&self) -> Vec<Ts2> {
let mut fields = vec![];
if let Some(_dims) = &self.emf_dimensions {
fields.push(quote! {
__config__: ::metrique::emf::SetEntryDimensions
})
}
fields
}
fn create_configuration(&self) -> Vec<Ts2> {
let mut fields = vec![];
if let Some(dims) = &self.emf_dimensions {
fields
.push(quote! { __config__: ::metrique::__plumbing_entry_dimensions!(dims: #dims) })
}
fields
}
}
#[derive(Debug, FromField)]
#[darling(attributes(metrics))]
struct RawMetricsFieldAttrs {
flatten: Flag,
flatten_entry: Flag,
timestamp: Flag,
ignore: Flag,
#[darling(default)]
unit: Option<SpannedKv<syn::Path>>,
#[darling(default)]
format: Option<SpannedKv<syn::Path>>,
#[darling(default)]
name: Option<SpannedKv<String>>,
}
#[derive(Debug)]
struct SpannedKv<T> {
key_span: Span,
#[allow(dead_code)]
value_span: Span,
value: T,
}
impl<T: FromMeta> FromMeta for SpannedKv<T> {
fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
let value = T::from_meta(item).map_err(|e| e.with_span(item))?;
let (key_span, value_span) = match item {
syn::Meta::NameValue(nv) => (nv.path.span(), nv.value.span()),
_ => return Err(darling::Error::custom("expected a key value pair").with_span(item)),
};
Ok(SpannedKv {
key_span,
value_span,
value,
})
}
}
fn set_exclusive(
new: impl Fn(Span) -> MetricsFieldAttrs,
name: &'static str,
existing: Option<(MetricsFieldAttrs, &'static str)>,
flag: &Flag,
) -> darling::Result<Option<(MetricsFieldAttrs, &'static str)>> {
match (flag.is_present(), &existing) {
(true, Some((_, other))) => Err(darling::Error::custom(format!(
"Cannot combine {other} with {name}"
))
.with_span(&flag.span())),
(true, None) => Ok(Some((new(flag.span()), name))),
_ => Ok(existing),
}
}
fn get_field_option<'a, T>(
field_name: &'static str,
existing: &Option<(MetricsFieldAttrs, &'static str)>,
span: &'a Option<SpannedKv<T>>,
) -> darling::Result<Option<&'a T>> {
match (span, &existing) {
(Some(input), Some((_, other))) => Err(darling::Error::custom(format!(
"Cannot combine {other} with {field_name}"
))
.with_span(&input.key_span)),
(Some(v), None) => Ok(Some(&v.value)),
_ => Ok(None),
}
}
impl RawMetricsFieldAttrs {
fn validate(self) -> darling::Result<MetricsFieldAttrs> {
let mut out: Option<(MetricsFieldAttrs, &'static str)> = None;
out = set_exclusive(MetricsFieldAttrs::Flatten, "flatten", out, &self.flatten)?;
out = set_exclusive(
MetricsFieldAttrs::FlattenEntry,
"flatten_entry",
out,
&self.flatten_entry,
)?;
out = set_exclusive(
MetricsFieldAttrs::Timestamp,
"timestamp",
out,
&self.timestamp,
)?;
out = set_exclusive(MetricsFieldAttrs::Ignore, "ignore", out, &self.ignore)?;
let name = self.name.map(validate_name).transpose()?;
let name = get_field_option("name", &out, &name)?;
let unit = get_field_option("unit", &out, &self.unit)?;
let format = get_field_option("format", &out, &self.format)?;
Ok(match out {
Some((out, _)) => out,
None => MetricsFieldAttrs::Field {
name: name.cloned(),
unit: unit.cloned(),
format: format.cloned(),
},
})
}
}
fn validate_name(name: SpannedKv<String>) -> darling::Result<SpannedKv<String>> {
match validate_name_inner(&name.value) {
Ok(_) => Ok(name),
Err(msg) => Err(darling::Error::custom(msg).with_span(&name.value_span)),
}
}
fn validate_name_inner(name: &str) -> std::result::Result<(), &'static str> {
if name.is_empty() {
return Err("invalid name: name field must not be empty");
}
if name.contains(' ') {
return Err("invalid name: name must not contain spaces");
}
Ok(())
}
#[derive(Debug, Clone)]
enum MetricsFieldAttrs {
Ignore(Span),
Flatten(Span),
FlattenEntry(Span),
Timestamp(Span),
Field {
unit: Option<syn::Path>,
name: Option<String>,
format: Option<syn::Path>,
},
}
fn parse_root_attrs(attr: TokenStream) -> Result<RootAttributes> {
let nested_meta = NestedMeta::parse_meta_list(attr.into())?;
Ok(RootAttributes::from_list(&nested_meta)?)
}
fn generate_metrics(root_attributes: RootAttributes, input: DeriveInput) -> Result<Ts2> {
let struct_name = &input.ident;
let entry_name = format_ident!("{}Entry", struct_name);
let guard_name = format_ident!("{}Guard", struct_name);
let handle_name = format_ident!("{}Handle", struct_name);
let fields = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(fields_named) => &fields_named.named,
_ => {
return Err(Error::new_spanned(
&input,
"Only named fields are supported",
));
}
},
_ => return Err(Error::new_spanned(&input, "Only structs are supported")),
};
let parsed_fields = parse_struct_fields(fields)?;
let base_struct = generate_base_struct(
struct_name,
&input.vis,
&input.generics,
&input.attrs,
&parsed_fields,
)?;
let entry_struct = generate_entry_struct(
&entry_name,
&input.generics,
&parsed_fields,
&root_attributes,
)?;
let entry_impl = entry_impl::generate_entry_impl(&entry_name, &parsed_fields, &root_attributes);
let close_value_impl =
generate_close_value_impl(struct_name, &entry_name, &parsed_fields, &root_attributes);
let vis = &input.vis;
let root_entry_specifics = if root_attributes.subfield.is_present() {
quote! {}
} else {
let on_drop_wrapper =
generate_on_drop_wrapper(vis, &guard_name, struct_name, &entry_name, &handle_name);
quote! {
#on_drop_wrapper
}
};
let output = quote! {
#base_struct
#entry_struct
#entry_impl
#close_value_impl
#root_entry_specifics
};
if std::env::var("MACRO_DEBUG").is_ok() {
eprintln!("{}", &output);
}
Ok(output)
}
fn generate_base_struct(
name: &Ident,
vis: &Visibility,
generics: &Generics,
attrs: &[Attribute],
fields: &[MetricsField],
) -> Result<Ts2> {
let fields = fields.iter().map(|f| f.core_field());
let data = quote! {
#(#fields),*
};
let expanded = quote! {
#(#attrs)*
#vis struct #name #generics { #data }
};
Ok(expanded)
}
fn generate_on_drop_wrapper(
vis: &Visibility,
guard: &Ident,
inner: &Ident,
target: &Ident,
handle: &Ident,
) -> Ts2 {
quote! {
#vis type #guard<Q = ::metrique::DefaultSink> = ::metrique::AppendAndCloseOnDrop<#inner, Q>;
#vis type #handle<Q = ::metrique::DefaultSink> = ::metrique::AppendAndCloseOnDropHandle<#inner, Q>;
impl #inner {
#[doc = "Creates a AppendAndCloseOnDrop that will be automatically appended to `sink` on drop."]
#vis fn append_on_drop<Q: ::metrique::__writer::EntrySink<::metrique::RootEntry<#target>> + Send + Sync + 'static>(self, sink: Q) -> #guard<Q> {
::metrique::append_and_close(self, sink)
}
}
}
}
fn generate_close_value_impl(
metrics_struct: &Ident,
entry: &Ident,
fields: &[MetricsField],
root_attrs: &RootAttributes,
) -> Ts2 {
let fields = fields
.iter()
.filter(|f| !matches!(f.attrs, MetricsFieldAttrs::Ignore(_)))
.map(|f| f.close_value());
let config = root_attrs.create_configuration();
quote! {
impl metrique::CloseValue for #metrics_struct {
type Closed = #entry;
fn close(self) -> Self::Closed {
#[allow(deprecated)]
#entry {
#(#config,)*
#(#fields,)*
}
}
}
}
}
fn generate_entry_struct(
name: &Ident,
_generics: &Generics,
fields: &[MetricsField],
root_attrs: &RootAttributes,
) -> Result<Ts2> {
let fields = fields.iter().flat_map(|f| f.entry_field());
let config = root_attrs.configuration_fields();
let data = quote! {
#(#config,)*
#(#fields,)*
};
let expanded = quote! {
#[doc(hidden)]
pub struct #name {
#data
}
};
Ok(expanded)
}
fn parse_struct_fields(
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> Result<Vec<MetricsField>> {
let mut parsed_fields = vec![];
let mut errors = darling::Error::accumulator();
for field in fields {
let field_name = field
.ident
.as_ref()
.ok_or_else(|| Error::new_spanned(field, "Field must have a name"))?;
let attrs = match errors
.handle(RawMetricsFieldAttrs::from_field(field).and_then(|attr| attr.validate()))
{
Some(attrs) => attrs,
None => {
continue;
}
};
let mut external_attrs = vec![];
for attr in &field.attrs {
if !attr.path().is_ident("metrics") {
external_attrs.push(attr.clone());
}
}
parsed_fields.push(MetricsField {
ident: field_name.clone(),
ty: field.ty.clone(),
vis: field.vis.clone(),
external_attrs,
attrs,
});
}
errors.finish()?;
Ok(parsed_fields)
}
struct MetricsField {
vis: Visibility,
ident: Ident,
ty: Type,
external_attrs: Vec<Attribute>,
attrs: MetricsFieldAttrs,
}
impl MetricsField {
fn core_field(&self) -> Ts2 {
let MetricsField {
ref external_attrs,
ref ident,
ref ty,
ref vis,
..
} = *self;
quote! { #(#external_attrs)* #vis #ident: #ty }
}
fn entry_field(&self) -> Option<Ts2> {
let ident_span = self.ident.span();
if let MetricsFieldAttrs::Ignore(_span) = self.attrs {
return None;
}
let &MetricsField { ident, ty, .. } = &self;
let mut base_type = quote_spanned! { ident_span=>
<#ty as metrique::CloseValue>::Closed
};
if let MetricsFieldAttrs::FlattenEntry(_) = self.attrs {
base_type = quote_spanned! { ident_span=>#ty };
}
if let Some(expr) = self.unit() {
base_type = quote_spanned! { expr.span()=>
<#base_type as ::metrique::unit::AttachUnit>::Output<#expr>
}
}
Some(quote_spanned! { ident_span=>
#[deprecated(note = "these fields will become private in a future release. To introspect an entry, use `metrique_writer::test_util::test_entry`")]
#[doc(hidden)]
#ident: #base_type
})
}
fn unit(&self) -> Option<&syn::Path> {
match &self.attrs {
MetricsFieldAttrs::Field { unit, .. } => unit.as_ref(),
_ => None,
}
}
fn close_value(&self) -> Ts2 {
let ident = &self.ident;
let base = if let MetricsFieldAttrs::FlattenEntry(_) = self.attrs {
quote_spanned! { ident.span() => self.#ident }
} else {
let mut base = quote_spanned! {
ident.span() => metrique::CloseValue::close(self.#ident)
};
if let Some(unit) = self.unit() {
base = quote_spanned! { unit.span() =>
#base.into()
}
}
base
};
quote! { #ident: #base }
}
}
fn clean_base_struct(input: &DeriveInput) -> Ts2 {
let struct_name = &input.ident;
let vis = &input.vis;
let generics = &input.generics;
let filtered_attrs: Vec<_> = input
.attrs
.iter()
.filter(|attr| !attr.path().is_ident("metrics"))
.collect();
let fields = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(fields_named) => &fields_named.named,
_ => return input.to_token_stream(),
},
_ => return input.to_token_stream(),
};
let clean_fields = fields.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_type = &field.ty;
let field_vis = &field.vis;
let field_attrs: Vec<_> = field
.attrs
.iter()
.filter(|attr| !attr.path().is_ident("metrics"))
.collect();
quote! {
#(#field_attrs)*
#field_vis #field_name: #field_type
}
});
let expanded = quote! {
#(#filtered_attrs)*
#vis struct #struct_name #generics {
#(#clean_fields),*
}
};
expanded
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use proc_macro2::TokenStream as Ts2;
use quote::quote;
use syn::{parse_quote, parse2};
use crate::RootAttributes;
fn metrics_impl(input: Ts2) -> Ts2 {
let input = syn::parse2(input).unwrap();
let root_attrs = RootAttributes::default();
super::generate_metrics(root_attrs, input).unwrap()
}
#[test]
fn test_darling_root_attrs() {
use darling::FromMeta;
RootAttributes::from_meta(&parse_quote! {
metrics(
rename_all = "PascalCase",
emf::dimension_sets = [["bar"]]
)
})
.unwrap();
}
#[test]
fn test_simple_metrics_struct() {
let input = quote! {
struct RequestMetrics {
operation: &'static str,
number_of_ducks: usize
}
};
let output = metrics_impl(input);
let parsed_file = match parse2::<syn::File>(output.clone()) {
Ok(file) => prettyplease::unparse(&file),
Err(_) => {
output.to_string()
}
};
assert_snapshot!("simple_metrics_struct", parsed_file);
}
}