diff --git a/autometrics-macros/Cargo.toml b/autometrics-macros/Cargo.toml index 881163b..97d3513 100644 --- a/autometrics-macros/Cargo.toml +++ b/autometrics-macros/Cargo.toml @@ -14,6 +14,7 @@ categories = { workspace = true } proc-macro = true [dependencies] +Inflector = "0.11.4" percent-encoding = "2.2" proc-macro2 = "1" quote = "1" diff --git a/autometrics-macros/src/lib.rs b/autometrics-macros/src/lib.rs index 41b6bc2..4939025 100644 --- a/autometrics-macros/src/lib.rs +++ b/autometrics-macros/src/lib.rs @@ -3,6 +3,7 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use proc_macro2::{Ident, TokenStream}; use quote::quote; use std::env; +use inflector::Inflector; use syn::{parse_macro_input, DeriveInput, ImplItem, ItemFn, ItemImpl, Result, Data, DataEnum, Attribute, Meta, NestedMeta, Lit}; mod parse; @@ -129,10 +130,10 @@ pub fn autometrics( output.into() } -#[proc_macro_derive(LabelValues, attributes(autometrics))] -pub fn derive_label_values(input: proc_macro::TokenStream) -> proc_macro::TokenStream { +#[proc_macro_derive(GetLabel, attributes(autometrics))] +pub fn derive_get_label(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); - let result = derive_label_values_impl(input); + let result = derive_get_label_impl(input); let output = match result { Ok(output) => output, Err(err) => err.into_compile_error(), @@ -188,9 +189,9 @@ fn instrument_function(args: &AutometricsArgs, item: ItemFn) -> Result Result { +fn derive_get_label_impl(input: DeriveInput) -> Result { let variants = match input.data { Data::Enum(DataEnum { variants, .. }) => variants, _ => { @@ -397,14 +398,41 @@ fn derive_label_values_impl(input: DeriveInput) -> Result { }, }; - let match_arms = variants + let label_key = { + let attrs: Vec<_> = input.attrs.iter().filter(|attr| attr.path.is_ident("autometrics")).collect(); + + let key_from_attr = match attrs.len() { + 0 => None, + 1 => get_label_attr(attrs[0], "label_key")?, + _ => { + let mut error = + syn::Error::new_spanned(attrs[1], "redundant `autometrics(label_value)` attribute"); + error.combine(syn::Error::new_spanned(attrs[0], "note: first one here")); + return Err(error); + } + }; + + let key_from_attr = key_from_attr.map(|value| value.to_string()); + + // Check casing of the user-provided value + if let Some(key) = &key_from_attr { + if key.as_str() != key.to_snake_case() { + return Err(syn::Error::new_spanned(attrs[0], "label_key should be snake_cased")); + } + } + + let ident = input.ident.clone(); + key_from_attr.unwrap_or_else(|| ident.clone().to_string().to_snake_case()) + }; + + let value_match_arms = variants .into_iter() .map(|variant| { let attrs: Vec<_> = variant.attrs.iter().filter(|attr| attr.path.is_ident("autometrics")).collect(); let value_from_attr = match attrs.len() { 0 => None, - 1 => get_label_value_attr(attrs[0])?, + 1 => get_label_attr(attrs[0], "label_value")?, _ => { let mut error = syn::Error::new_spanned(attrs[1], "redundant `autometrics(label_value)` attribute"); @@ -413,11 +441,19 @@ fn derive_label_values_impl(input: DeriveInput) -> Result { } }; + let value_from_attr = value_from_attr.map(|value| value.to_string()); + + // Check casing of the user-provided value + if let Some(value) = &value_from_attr { + if value.as_str() != value.to_snake_case() { + return Err(syn::Error::new_spanned(attrs[0], "label_value should be snake_cased")); + } + } + let ident = variant.ident; - let value = value_from_attr.unwrap_or_else(|| ident.clone()); - let value = value.to_string(); + let value = value_from_attr.unwrap_or_else(|| ident.clone().to_string().to_snake_case()); Ok(quote! { - Self::#ident => Some(#value), + Self::#ident => #value, }) }) .collect::>()?; @@ -425,17 +461,17 @@ fn derive_label_values_impl(input: DeriveInput) -> Result { let ident = input.ident; Ok(quote! { #[automatically_derived] - impl GetLabelValue for #ident { - fn get_label_value(&self) -> Option<&'static str> { - match self { - #match_arms - } + impl GetLabel for #ident { + fn get_label(&self) -> Option<(&'static str, &'static str)> { + Some((#label_key, match self { + #value_match_arms + })) } } }) } -fn get_label_value_attr(attr: &Attribute) -> Result> { +fn get_label_attr(attr: &Attribute, attr_name: &str) -> Result> { let meta = attr.parse_meta()?; let meta_list = match meta { Meta::List(list) => list, @@ -456,13 +492,13 @@ fn get_label_value_attr(attr: &Attribute) -> Result> { let label_value = match nested { NestedMeta::Meta(Meta::NameValue(nv)) => nv, - _ => return Err(syn::Error::new_spanned(nested, "expected `label_value = \"\"`")), + _ => return Err(syn::Error::new_spanned(nested, format!("expected `{attr_name} = \"\"`"))), }; - if !label_value.path.is_ident("label_value") { + if !label_value.path.is_ident(attr_name) { return Err(syn::Error::new_spanned( &label_value.path, - "unsupported autometrics attribute, expected `label_value`", + format!("unsupported autometrics attribute, expected `{attr_name}`"), )); } diff --git a/autometrics/src/labels.rs b/autometrics/src/labels.rs index d83a142..ee8dda2 100644 --- a/autometrics/src/labels.rs +++ b/autometrics/src/labels.rs @@ -132,10 +132,7 @@ pub trait GetLabelsFromResult { impl GetLabelsFromResult for Result { fn __autometrics_get_labels(&self) -> Option { - match self { - Ok(ok) => Some((OK_KEY, ok.get_label_value())), - Err(err) => Some((ERROR_KEY, err.get_label_value())), - } + self.get_label().map(|(k, v)| (k, Some(v))) } } @@ -202,64 +199,65 @@ macro_rules! impl_trait_for_types { impl_trait_for_types!(GetLabels); -pub trait GetLabelValue { - fn get_label_value(&self) -> Option<&'static str> { +pub trait GetLabel { + fn get_label(&self) -> Option<(&'static str, &'static str)> { None } } -impl_trait_for_types!(GetLabelValue); +impl_trait_for_types!(GetLabel); #[cfg(test)] mod tests { use super::*; - use autometrics_macros::LabelValues; + use autometrics_macros::GetLabel; #[test] fn custom_trait_implementation() { struct CustomResult; - impl GetLabelValue for CustomResult { - fn get_label_value(&self) -> Option<&'static str> { - Some("my-result") + impl GetLabel for CustomResult { + fn get_label(&self) -> Option<(&'static str, &'static str)> { + Some(("ok", "my-result")) } } - assert_eq!(Some("my-result"), CustomResult {}.get_label_value()); + assert_eq!(Some(("ok", "my-result")), CustomResult {}.get_label()); } #[test] fn manual_enum() { - enum Foo { + enum MyFoo { A, B, } - impl GetLabelValue for Foo { - fn get_label_value(&self) -> Option<&'static str> { - match self { - Foo::A => Some("a"), - Foo::B => Some("b"), - } + impl GetLabel for MyFoo { + fn get_label(&self) -> Option<(&'static str, &'static str)> { + Some(("hello", match self { + MyFoo::A => "a", + MyFoo::B => "b", + })) } } - assert_eq!(Some("a"), Foo::A.get_label_value()); - assert_eq!(Some("b"), Foo::B.get_label_value()); + assert_eq!(Some(("hello", "a")), MyFoo::A.get_label()); + assert_eq!(Some(("hello", "b")), MyFoo::B.get_label()); } #[test] fn derived_enum() { - #[derive(LabelValues)] - enum Foo { + #[derive(GetLabel)] + #[autometrics(label_key = "my_foo")] + enum MyFoo { #[autometrics(label_value = "hello")] - A, + Alpha, #[autometrics()] - B, - C, + BetaValue, + Charlie, } - assert_eq!(Some("hello"), Foo::A.get_label_value()); - assert_eq!(Some("B"), Foo::B.get_label_value()); - assert_eq!(Some("C"), Foo::C.get_label_value()); + assert_eq!(Some(("my_foo", "hello")), MyFoo::Alpha.get_label()); + assert_eq!(Some(("my_foo", "beta_value")), MyFoo::BetaValue.get_label()); + assert_eq!(Some(("my_foo", "charlie")), MyFoo::Charlie.get_label()); } } diff --git a/autometrics/src/lib.rs b/autometrics/src/lib.rs index 8440499..84ed1ff 100644 --- a/autometrics/src/lib.rs +++ b/autometrics/src/lib.rs @@ -14,7 +14,7 @@ mod task_local; mod tracker; pub use autometrics_macros::autometrics; -pub use labels::GetLabelValue; +pub use labels::GetLabel; pub use objectives::{Objective, ObjectivePercentage, TargetLatency}; // Optional exports