#![deny(missing_docs)]
#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_cfg))]
mod emf;
mod entry_impl;
mod enums;
mod inflect;
mod structs;
mod value_impl;
use darling::{
FromField, FromMeta,
ast::NestedMeta,
util::{Flag, SpannedValue},
};
use emf::DimensionSets;
use inflect::NameStyle;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as Ts2};
use quote::{ToTokens, quote, quote_spanned};
use syn::{
Attribute, Data, DeriveInput, Error, Fields, Ident, Result, Type, Visibility,
parse_macro_input, spanned::Spanned,
};
use crate::inflect::{
metric_name, name_contains_dot, name_contains_uninflectables, name_ends_with_delimiter,
};
#[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_adt(&input).to_tokens(&mut base_token_stream);
err.to_compile_error().to_tokens(&mut base_token_stream);
}
};
base_token_stream.into()
}
#[derive(Copy, Clone, Debug)]
enum OwnershipKind {
ByRef,
ByValue,
}
#[derive(Debug, Default, FromMeta)]
#[darling(from_word = Self::from_word)]
struct ValueAttributes {
string: Flag,
}
impl ValueAttributes {
fn from_word() -> darling::Result<Self> {
Ok(Self::default())
}
}
#[derive(Debug, Default, FromMeta)]
struct RawRootAttributes {
prefix: Option<SpannedKv<String>>,
exact_prefix: Option<SpannedKv<String>>,
#[darling(default)]
rename_all: NameStyle,
#[darling(rename = "emf::dimension_sets")]
emf_dimensions: Option<DimensionSets>,
subfield: Flag,
#[darling(rename = "subfield_owned")]
subfield_owned: Flag,
#[darling(rename = "sample_group")]
sample_group: Flag,
value: Option<ValueAttributes>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
enum MetricMode {
#[default]
RootEntry,
Subfield,
SubfieldOwned,
Value,
ValueString,
}
#[derive(Debug, Default)]
struct RootAttributes {
prefix: Option<Prefix>,
rename_all: NameStyle,
emf_dimensions: Option<DimensionSets>,
sample_group: bool,
mode: MetricMode,
}
impl RawRootAttributes {
fn validate(self) -> darling::Result<RootAttributes> {
let mut out: Option<(MetricMode, &'static str)> = None;
if let Some(value_attrs) = self.value {
if value_attrs.string.is_present() {
out = set_exclusive(
|_| MetricMode::ValueString,
"value",
out,
&value_attrs.string,
)?
} else {
out = Some((MetricMode::Value, "value"));
}
}
out = set_exclusive(|_| MetricMode::Subfield, "subfield", out, &self.subfield)?;
out = set_exclusive(
|_| MetricMode::SubfieldOwned,
"subfield_owned",
out,
&self.subfield_owned,
)?;
let mut mode = out.map(|(s, _)| s).unwrap_or_default();
let sample_group = if self.sample_group.is_present() {
if let MetricMode::Value = &mut mode {
true
} else {
return Err(darling::Error::custom(
"`sample_group` as a top-level attribute can only be used with #[metrics(value)]",
)
.with_span(&self.sample_group.span()));
}
} else {
false
};
if let (MetricMode::ValueString, Some(ds)) = (mode, &self.emf_dimensions) {
return Err(
darling::Error::custom("value does not make sense with dimension-sets")
.with_span(&ds.span()),
);
}
Ok(RootAttributes {
prefix: Prefix::from_inflectable_and_exact(
&self.prefix,
&self.exact_prefix,
PrefixLevel::Root,
)?
.map(SpannedValue::into_inner),
rename_all: self.rename_all,
emf_dimensions: self.emf_dimensions,
sample_group,
mode,
})
}
}
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
}
fn ownership_kind(&self) -> OwnershipKind {
match self.mode {
MetricMode::RootEntry | MetricMode::SubfieldOwned => OwnershipKind::ByValue,
MetricMode::Subfield | MetricMode::ValueString | MetricMode::Value => {
OwnershipKind::ByRef
}
}
}
fn warnings(&self) -> Ts2 {
quote! {}
}
}
#[derive(Debug, FromField)]
#[darling(attributes(metrics))]
struct RawMetricsFieldAttrs {
flatten: Flag,
flatten_entry: Flag,
no_close: Flag,
timestamp: Flag,
sample_group: Flag,
ignore: Flag,
#[darling(default)]
unit: Option<SpannedKv<syn::Path>>,
#[darling(default)]
format: Option<SpannedKv<syn::Path>>,
#[darling(default)]
name: Option<SpannedKv<String>>,
#[darling(default)]
prefix: Option<SpannedKv<String>>,
#[darling(default)]
exact_prefix: Option<SpannedKv<String>>,
}
#[derive(Debug)]
pub(crate) struct SpannedKv<T> {
pub(crate) key_span: Span,
#[allow(dead_code)]
pub(crate) value_span: Span,
pub(crate) 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 cannot_combine_error(existing: &str, new: &str, new_span: Span) -> darling::Error {
darling::Error::custom(format!("Cannot combine `{existing}` with `{new}`")).with_span(&new_span)
}
fn set_exclusive<T>(
new: impl Fn(Span) -> T,
name: &'static str,
existing: Option<(T, &'static str)>,
flag: &Flag,
) -> darling::Result<Option<(T, &'static str)>> {
match (flag.is_present(), &existing) {
(true, Some((_, other))) => Err(cannot_combine_error(other, name, flag.span())),
(true, None) => Ok(Some((new(flag.span()), name))),
_ => Ok(existing),
}
}
fn get_field_option<'a, T>(
field_name: &'static str,
existing: &Option<(MetricsFieldKind, &'static str)>,
span: &'a Option<SpannedKv<T>>,
) -> darling::Result<Option<&'a T>> {
match (span, &existing) {
(Some(input), Some((_, other))) => {
Err(cannot_combine_error(other, field_name, input.key_span))
}
(Some(v), None) => Ok(Some(&v.value)),
_ => Ok(None),
}
}
fn get_field_flag(
field_name: &'static str,
existing: &Option<(MetricsFieldKind, &'static str)>,
flag: &Flag,
) -> darling::Result<Option<Span>> {
match (flag.is_present(), &existing) {
(true, Some((_, other))) => Err(cannot_combine_error(other, field_name, flag.span())),
(true, None) => Ok(Some(flag.span())),
_ => Ok(None),
}
}
impl RawMetricsFieldAttrs {
fn validate(self) -> darling::Result<MetricsFieldAttrs> {
let mut out: Option<(MetricsFieldKind, &'static str)> = None;
out = set_exclusive(
|span| MetricsFieldKind::Flatten { span, prefix: None },
"flatten",
out,
&self.flatten,
)?;
out = set_exclusive(
MetricsFieldKind::FlattenEntry,
"flatten_entry",
out,
&self.flatten_entry,
)?;
out = set_exclusive(
MetricsFieldKind::Timestamp,
"timestamp",
out,
&self.timestamp,
)?;
out = set_exclusive(MetricsFieldKind::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)?;
let sample_group = get_field_flag("sample_group", &out, &self.sample_group)?;
let close = !self.no_close.is_present();
if let (false, Some((MetricsFieldKind::Ignore(span), _))) = (close, &out) {
return Err(cannot_combine_error("no_close", "ignore", *span));
}
let prefix = Prefix::from_inflectable_and_exact(
&self.prefix,
&self.exact_prefix,
PrefixLevel::Field,
)?;
if let Some(prefix_) = prefix {
match &mut out {
Some((MetricsFieldKind::Flatten { prefix, .. }, _)) => {
*prefix = Some(prefix_.into_inner());
}
_ => {
return Err(
darling::Error::custom("prefix can only be used with `flatten`")
.with_span(&prefix_.span()),
);
}
}
}
Ok(MetricsFieldAttrs {
close,
kind: match out {
Some((out, _)) => out,
None => MetricsFieldKind::Field {
sample_group,
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)]
struct MetricsFieldAttrs {
close: bool,
kind: MetricsFieldKind,
}
pub(crate) struct MetricsField {
pub(crate) vis: Visibility,
pub(crate) ident: Ts2,
pub(crate) name: Option<String>,
pub(crate) span: Span,
pub(crate) ty: Type,
pub(crate) external_attrs: Vec<Attribute>,
pub(crate) attrs: MetricsFieldAttrs,
}
impl MetricsField {
fn core_field(&self, is_named: bool) -> Ts2 {
let MetricsField {
ref external_attrs,
ref ident,
ref ty,
ref vis,
..
} = *self;
let field = if is_named {
quote! { #ident: #ty }
} else {
quote! { #ty }
};
quote! { #(#external_attrs)* #vis #field }
}
fn entry_field(&self, named: bool) -> Option<Ts2> {
if let MetricsFieldKind::Ignore(_span) = self.attrs.kind {
return None;
}
let MetricsField {
ident, ty, span, ..
} = self;
let mut base_type = if self.attrs.close {
quote_spanned! { *span=> <#ty as metrique::CloseValue>::Closed }
} else {
quote_spanned! { *span=>#ty }
};
if let Some(expr) = self.unit() {
base_type = quote_spanned! { expr.span()=>
<#base_type as ::metrique::unit::AttachUnit>::Output<#expr>
}
}
let inner = if named {
quote! { #ident: #base_type }
} else {
quote! { #base_type }
};
Some(quote_spanned! { *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)]
#inner
})
}
fn unit(&self) -> Option<&syn::Path> {
match &self.attrs.kind {
MetricsFieldKind::Field { unit, .. } => unit.as_ref(),
_ => None,
}
}
pub(crate) fn close_value(&self, ownership_kind: OwnershipKind) -> Ts2 {
let ident = &self.ident;
let span = self.span;
let field_expr = match ownership_kind {
OwnershipKind::ByValue => quote_spanned! {span=> self.#ident },
OwnershipKind::ByRef => quote_spanned! {span=> &self.#ident },
};
let base = if self.attrs.close {
quote_spanned! {span=> metrique::CloseValue::close(#field_expr) }
} else {
field_expr
};
let base = if let Some(unit) = self.unit() {
quote_spanned! { unit.span() =>
#base.into()
}
} else {
base
};
quote! { #ident: #base }
}
}
pub(crate) enum PrefixLevel {
Root,
Field,
}
#[derive(Debug, Clone)]
pub(crate) enum Prefix {
Inflectable { prefix: String },
Exact(String),
}
impl Prefix {
fn inflected_prefix_message(prefix: &str, c: char) -> String {
let warning_text = if name_contains_dot(prefix) {
" '.' used to be allowed in `prefix` but is now forbidden."
} else {
""
};
let prefix_fixed: String = prefix
.chars()
.map(|c| if !c.is_alphanumeric() { '-' } else { c })
.collect();
format!(
"You cannot use the character {c:?} with `prefix`. `prefix` will \"inflect\" to match the name scheme specified by `rename_all`. For example, \
it will change all delimiters to `-` for kebab case). If you want to match namestyle, use `prefix = {prefix_fixed:?}`. If you want to preserve {c:?} \
in the final metric name use `exact_prefix = {prefix:?}.{warning_text}"
)
}
fn prefix_should_end_with_delimiter_message(prefix: &str) -> String {
let delimiter = if prefix.contains('-') { '-' } else { '_' };
let prefix_fixed = format!("{prefix}{delimiter}");
format!(
"The root-level prefix `{prefix:?}` must end with a delimiter. Use `prefix = {prefix_fixed:?}`, which inflects \
correctly in all inflections"
)
}
fn from_inflectable_and_exact(
inflectable: &Option<SpannedKv<String>>,
exact: &Option<SpannedKv<String>>,
level: PrefixLevel,
) -> darling::Result<Option<SpannedValue<Self>>> {
match (inflectable, exact) {
(Some(prefix), None) => {
if let Some(c) = name_contains_uninflectables(&prefix.value) {
Err(
darling::Error::custom(Self::inflected_prefix_message(&prefix.value, c))
.with_span(&prefix.key_span),
)
} else if let PrefixLevel::Root = level
&& !name_ends_with_delimiter(&prefix.value)
{
Err(
darling::Error::custom(Self::prefix_should_end_with_delimiter_message(
&prefix.value,
))
.with_span(&prefix.key_span),
)
} else {
Ok(Some(SpannedValue::new(
Self::Inflectable {
prefix: prefix.value.clone(),
},
prefix.key_span,
)))
}
}
(None, Some(p)) => Ok(Some(SpannedValue::new(
Prefix::Exact(p.value.clone()),
p.key_span,
))),
(None, None) => Ok(None),
(Some(inflectable), Some(_)) => Err(cannot_combine_error(
"prefix",
"exact_prefix",
inflectable.key_span,
)),
}
}
}
#[derive(Debug, Clone)]
enum MetricsFieldKind {
Ignore(Span),
Flatten {
span: Span,
prefix: Option<Prefix>,
},
FlattenEntry(Span),
Timestamp(Span),
Field {
unit: Option<syn::Path>,
name: Option<String>,
format: Option<syn::Path>,
sample_group: Option<Span>,
},
}
#[allow(unused)]
fn proc_macro_warning(span: Span, warning: &str) -> Ts2 {
quote_spanned! {span=>
const _: () = {
#[deprecated(note=#warning)]
const _W: () = ();
_W
};
}
}
fn parse_root_attrs(attr: TokenStream) -> Result<RootAttributes> {
let nested_meta = NestedMeta::parse_meta_list(attr.into())?;
Ok(RawRootAttributes::from_list(&nested_meta)?.validate()?)
}
fn generate_metrics(root_attributes: RootAttributes, input: DeriveInput) -> Result<Ts2> {
let output = match root_attributes.mode {
MetricMode::RootEntry
| MetricMode::Subfield
| MetricMode::SubfieldOwned
| MetricMode::Value => {
let fields = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(fields_named) => &fields_named.named,
Fields::Unnamed(fields_unnamed)
if root_attributes.mode == MetricMode::Value =>
{
&fields_unnamed.unnamed
}
_ => {
return Err(Error::new_spanned(
&input,
"Only named fields are supported",
));
}
},
_ => {
return Err(Error::new_spanned(
&input,
"Only structs are supported for entries",
));
}
};
structs::generate_metrics_for_struct(root_attributes, &input, fields)?
}
MetricMode::ValueString => {
let variants = match &input.data {
Data::Enum(data_enum) => &data_enum.variants,
_ => {
return Err(Error::new_spanned(
&input,
"Only enums are supported for values",
));
}
};
enums::generate_metrics_for_enum(root_attributes, &input, variants)?
}
};
if std::env::var("MACRO_DEBUG").is_ok() {
eprintln!("{}", &output);
}
Ok(output)
}
pub(crate) fn generate_on_drop_wrapper(
vis: &Visibility,
guard: &Ident,
inner: &Ident,
target: &Ident,
handle: &Ident,
) -> Ts2 {
let inner_str = inner.to_string();
let guard_str = guard.to_string();
quote! {
#[doc = concat!("Metrics guard returned from [`", #inner_str, "::append_on_drop`], closes the entry and appends the metrics to a sink when dropped.")]
#vis type #guard<Q = ::metrique::DefaultSink> = ::metrique::AppendAndCloseOnDrop<#inner, Q>;
#[doc = concat!("Metrics handle returned from [`", #guard_str, "::handle`], similar to an `Arc<", #guard_str, ">`.")]
#vis type #handle<Q = ::metrique::DefaultSink> = ::metrique::AppendAndCloseOnDropHandle<#inner, Q>;
impl #inner {
#[doc = "Creates an 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_impls(
root_attrs: &RootAttributes,
base_ty: &Ident,
closed_ty: &Ident,
impl_body: Ts2,
) -> Ts2 {
let (metrics_struct_ty, proxy_impl) = match root_attrs.ownership_kind() {
OwnershipKind::ByValue => (quote!(#base_ty), quote!()),
OwnershipKind::ByRef => (
quote!(&'_ #base_ty),
quote!(impl metrique::CloseValue for #base_ty {
type Closed = #closed_ty;
fn close(self) -> Self::Closed {
<&Self>::close(&self)
}
}),
),
};
quote! {
impl metrique::CloseValue for #metrics_struct_ty {
type Closed = #closed_ty;
fn close(self) -> Self::Closed {
#impl_body
}
}
#proxy_impl
}
}
pub(crate) fn clean_attrs(attr: &[Attribute]) -> Vec<Attribute> {
attr.iter()
.filter(|attr| !attr.path().is_ident("metrics"))
.cloned()
.collect()
}
fn clean_base_adt(input: &DeriveInput) -> Ts2 {
let adt_name = &input.ident;
let vis = &input.vis;
let generics = &input.generics;
let filtered_attrs = clean_attrs(&input.attrs);
match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(fields_named) => {
structs::clean_base_struct(vis, adt_name, generics, filtered_attrs, fields_named)
}
Fields::Unnamed(fields_unnamed) => structs::clean_base_unnamed_struct(
vis,
adt_name,
generics,
filtered_attrs,
fields_unnamed,
),
_ => input.to_token_stream(),
},
Data::Enum(data_enum) => {
if let Ok(variants) = enums::parse_enum_variants(&data_enum.variants, false) {
enums::generate_base_enum(adt_name, vis, generics, &filtered_attrs, &variants)
} else {
input.to_token_stream()
}
}
_ => input.to_token_stream(),
}
}
#[cfg(test)]
mod tests {
use darling::FromMeta;
use insta::assert_snapshot;
use proc_macro2::TokenStream as Ts2;
use quote::quote;
use syn::{parse_quote, parse2};
use crate::RawRootAttributes;
fn metrics_impl(input: Ts2, attrs: Ts2) -> Ts2 {
let input = syn::parse2(input).unwrap();
let meta: syn::Meta = syn::parse2(attrs).unwrap();
let root_attrs = RawRootAttributes::from_meta(&meta)
.unwrap()
.validate()
.unwrap();
super::generate_metrics(root_attrs, input).unwrap()
}
fn metrics_impl_string(input: Ts2, attrs: Ts2) -> String {
let output = metrics_impl(input, attrs);
match parse2::<syn::File>(output.clone()) {
Ok(file) => prettyplease::unparse(&file),
Err(_) => {
output.to_string()
}
}
}
#[test]
fn test_darling_root_attrs() {
use darling::FromMeta;
RawRootAttributes::from_meta(&parse_quote! {
metrics(
rename_all = "PascalCase",
emf::dimension_sets = [["bar"]]
)
})
.unwrap()
.validate()
.unwrap();
}
#[test]
fn test_simple_metrics_struct() {
let input = quote! {
struct RequestMetrics {
operation: &'static str,
number_of_ducks: usize
}
};
let parsed_file = metrics_impl_string(input, quote!(metrics()));
assert_snapshot!("simple_metrics_struct", parsed_file);
}
#[test]
fn test_sample_group_metrics_struct() {
let input = quote! {
struct RequestMetrics {
#[metrics(sample_group)]
operation: &'static str,
number_of_ducks: usize
}
};
let parsed_file = metrics_impl_string(input, quote!(metrics()));
assert_snapshot!("sample_group_metrics_struct", parsed_file);
}
#[test]
fn test_simple_metrics_value_struct() {
let input = quote! {
struct RequestValue {
#[metrics(ignore)]
ignore: u32,
value: u32,
}
};
let parsed_file = metrics_impl_string(input, quote!(metrics(value)));
assert_snapshot!("simple_metrics_value_struct", parsed_file);
}
#[test]
fn test_sample_group_metrics_value_struct() {
let input = quote! {
struct RequestValue {
#[metrics(ignore)]
ignore: u32,
value: &'static str,
}
};
let parsed_file = metrics_impl_string(input, quote!(metrics(value, sample_group)));
assert_snapshot!("sample_group_metrics_value_struct", parsed_file);
}
#[test]
fn test_simple_metrics_value_unnamed_struct() {
let input = quote! {
struct RequestValue(
#[metrics(ignore)]
u32,
u32);
};
let parsed_file = metrics_impl_string(input, quote!(metrics(value)));
assert_snapshot!("simple_metrics_value_unnamed_struct", parsed_file);
}
#[test]
fn test_simple_metrics_enum() {
let input = quote! {
enum Foo {
Bar
}
};
let parsed_file = metrics_impl_string(input, quote!(metrics(value(string))));
assert_snapshot!("simple_metrics_enum", parsed_file);
}
#[test]
fn test_exact_prefix_struct() {
let input = quote! {
struct RequestMetrics {
operation: &'static str,
number_of_ducks: usize
}
};
let parsed_file = metrics_impl_string(input, quote!(metrics(exact_prefix = "API@")));
assert_snapshot!("exact_prefix_struct", parsed_file);
}
#[test]
fn test_field_exact_prefix_struct() {
let input = quote! {
struct RequestMetrics {
#[metrics(flatten, exact_prefix = "API@")]
nested: NestedMetrics,
operation: &'static str
}
};
let parsed_file = metrics_impl_string(input, quote!(metrics()));
assert_snapshot!("field_exact_prefix_struct", parsed_file);
}
}