use darling::FromMeta;
use darling::util::Flag;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{ToTokens, format_ident, quote};
use syn::punctuated::Punctuated;
use syn::{
Attribute, ExprStruct, Ident, LitStr, Path, Token, Type, Visibility, parse_macro_input,
parse_quote,
};
mod parsing;
mod validation;
#[derive(FromMeta)]
struct MacroArgs {
unprefixed: Flag,
#[darling(default = "Self::default_crate_path")]
crate_path: Path,
}
impl Default for MacroArgs {
fn default() -> Self {
Self {
unprefixed: Flag::default(),
crate_path: Self::default_crate_path(),
}
}
}
impl MacroArgs {
fn default_crate_path() -> Path {
parse_quote!(::foundations)
}
}
struct Mod {
attrs: Vec<Attribute>,
vis: Visibility,
mod_token: Token![mod],
ident: Ident,
fns: Vec<ItemFn>,
}
struct ItemFn {
attrs: FnAttrs,
vis: Visibility,
fn_token: Token![fn],
ident: Ident,
args: Punctuated<FnArg, Token![,]>,
arrow_token: Token![->],
ty: Type,
}
#[derive(Default)]
struct FnAttrs {
cfg: Vec<Attribute>,
doc: String,
ctor: Option<ExprStruct>,
optional: bool,
with_removal: bool,
}
struct FnArg {
attrs: ArgAttrs,
ident: Ident,
colon_token: Token![:],
ty: Type,
mode: ArgMode,
}
#[derive(Default)]
struct ArgAttrs {
serde: Option<Attribute>,
serde_as: Option<Attribute>,
}
enum ArgMode {
ByValue(Type),
Clone(Type),
Into(Type),
}
impl FnArg {
fn to_struct_member(&self) -> proc_macro2::TokenStream {
let Self {
attrs: ArgAttrs { serde, serde_as },
ident,
colon_token,
..
} = self;
let ty = match &self.mode {
ArgMode::ByValue(ty) => ty,
ArgMode::Clone(ty) => ty,
ArgMode::Into(ty) => ty,
};
quote! { #serde_as #serde #ident #colon_token #ty }
}
fn to_arg(&self) -> proc_macro2::TokenStream {
let Self {
ident,
colon_token,
ty,
..
} = self;
quote! { #ident #colon_token #ty }
}
fn to_initializer(&self) -> proc_macro2::TokenStream {
let Self {
ident, colon_token, ..
} = self;
match &self.mode {
ArgMode::ByValue(_) => quote! { #ident },
ArgMode::Clone(_) => quote! {
#ident #colon_token ::std::clone::Clone::clone(#ident)
},
ArgMode::Into(_) => quote! {
#ident #colon_token ::std::convert::Into::into(#ident)
},
}
}
}
pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as MacroArgs);
let mod_ = parse_macro_input!(item as Mod);
expand_from_parsed(args, mod_).into()
}
fn expand_from_parsed(args: MacroArgs, extern_: Mod) -> proc_macro2::TokenStream {
let MacroArgs {
unprefixed,
crate_path: foundations,
} = &args;
let with_service_prefix =
format_ident!("{}", !unprefixed.is_present(), span = unprefixed.span());
let Mod {
attrs: mod_attrs,
vis: mod_vis,
mod_token,
ident: mod_name,
fns,
} = extern_;
let metrics_struct = Ident::new(&format!("__{mod_name}_Metrics"), Span::call_site());
let metric_fields = fns.iter().map(|fn_| metric_field(foundations, fn_));
let label_set_structs = fns
.iter()
.filter_map(|fn_| label_set_struct(foundations, fn_));
let registry_init = |var: &str, opt: bool| {
let var = Ident::new(var, Span::call_site());
let optional = format_ident!("{opt}");
quote! {
let #var = &mut *#foundations::telemetry::metrics::internal::Registries::get_subsystem(
stringify!(#mod_name), #optional, #with_service_prefix
);
}
};
let init_registry = fns
.iter()
.any(|fn_| !fn_.attrs.optional)
.then(|| registry_init("registry", false));
let init_opt_registry = fns
.iter()
.any(|fn_| fn_.attrs.optional)
.then(|| registry_init("opt_registry", true));
let metric_inits = fns.iter().map(|fn_| metric_init(foundations, fn_));
let metric_fns = fns
.iter()
.map(|fn_| metric_fn(foundations, &metrics_struct, fn_));
quote! {
#(#mod_attrs)* #mod_vis #mod_token #mod_name {
use super::*;
#[allow(non_camel_case_types)]
struct #metrics_struct {
#(#metric_fields,)*
}
#(#label_set_structs)*
#[allow(non_upper_case_globals)]
static #metrics_struct: ::std::sync::LazyLock<#metrics_struct> =
::std::sync::LazyLock::new(|| {
#init_registry
#init_opt_registry
#metrics_struct {
#(#metric_inits,)*
}
});
#(#metric_fns)*
}
}
}
fn metric_field(foundations: &Path, fn_: &ItemFn) -> proc_macro2::TokenStream {
let ItemFn {
attrs: FnAttrs { cfg, ctor, .. },
args,
ty: metric_ty,
ident: metric_name,
..
} = fn_;
let field_ty = if args.is_empty() {
metric_ty.to_token_stream()
} else if let Some(ExprStruct {
path: ctor_path, ..
}) = ctor
{
quote! {
#foundations::reexports_for_macros::prometools::serde::Family<
#metric_name,
#metric_ty,
#ctor_path,
>
}
} else {
quote! {
#foundations::reexports_for_macros::prometools::serde::Family<
#metric_name,
#metric_ty,
>
}
};
quote! { #(#cfg)* #metric_name: #field_ty }
}
fn label_set_struct(foundations: &Path, fn_: &ItemFn) -> Option<proc_macro2::TokenStream> {
let ItemFn {
attrs: FnAttrs { cfg, .. },
args,
ident: label_set_name,
..
} = fn_;
if args.is_empty() {
return None;
}
let serde_with = quote! { #foundations::reexports_for_macros::serde_with };
let serde_with_str = LitStr::new(&serde_with.to_string(), Span::call_site());
let serde_as_attr = args
.iter()
.any(|arg| arg.attrs.serde_as.is_some())
.then(|| quote! { #[#serde_with::serde_as(crate = #serde_with_str)] });
let serde = quote! { #foundations::reexports_for_macros::serde };
let serde_str = LitStr::new(&serde.to_string(), Span::call_site());
let labels = args.iter().map(|arg| arg.to_struct_member());
Some(quote! {
#(#cfg)*
#[allow(non_camel_case_types)]
#serde_as_attr
#[derive(
::std::clone::Clone,
::std::cmp::Eq,
::std::hash::Hash,
::std::cmp::PartialEq,
#serde::Serialize,
)]
#[serde(crate = #serde_str)]
struct #label_set_name {
#(#labels,)*
}
})
}
fn metric_init(foundations: &Path, fn_: &ItemFn) -> proc_macro2::TokenStream {
let ItemFn {
attrs:
FnAttrs {
cfg,
doc,
optional,
ctor,
..
},
ident: field_name,
args,
..
} = fn_;
let reexports = quote! { #foundations::reexports_for_macros };
let registry = Ident::new(
if *optional {
"opt_registry"
} else {
"registry"
},
Span::call_site(),
);
if let Some(ctor) = ctor
&& let Some(path) = ctor.path.get_ident()
&& path == "HistogramBuilder"
&& let Err(err) = validation::validate_histogram_buckets(ctor)
{
return err.to_compile_error();
}
let metric_init = match ctor {
Some(ctor) if args.is_empty() => quote! {
#reexports::prometheus_client::metrics::family::MetricConstructor::new_metric(&(#ctor))
},
Some(ctor) => quote! {
#reexports::prometools::serde::Family::new_with_constructor(#ctor)
},
None => quote! { ::std::default::Default::default() },
};
quote! {
#(#cfg)*
#field_name: {
let metric = #metric_init;
#reexports::prometheus_client::registry::Registry::register(
#registry,
::std::stringify!(#field_name),
str::trim(#doc),
::std::boxed::Box::new(::std::clone::Clone::clone(&metric))
);
metric
}
}
}
fn metric_fn(foundations: &Path, metrics_struct: &Ident, fn_: &ItemFn) -> proc_macro2::TokenStream {
let ItemFn {
attrs:
FnAttrs {
cfg,
doc,
with_removal,
..
},
fn_token,
vis: fn_vis,
ident: metric_name,
args,
arrow_token,
ty: metric_type,
} = fn_;
let fn_args: Vec<_> = args.iter().map(|arg| arg.to_arg()).collect();
let (convert_args, access_metric) = if args.is_empty() {
let accessor = quote! {
::std::clone::Clone::clone(&#metrics_struct.#metric_name)
};
(quote! {}, accessor)
} else {
let label_inits = args.iter().map(|arg| arg.to_initializer());
let convert = quote! {
let __args = #metric_name {
#(#label_inits,)*
};
};
let accessor = quote! {
::std::clone::Clone::clone(
&#foundations::reexports_for_macros::prometools::serde::Family::get_or_create(
&#metrics_struct.#metric_name,
&__args,
)
)
};
(convert, accessor)
};
let removal_fns = if cfg!(foundations_unstable) && *with_removal {
let remove_ident = format_ident!("{metric_name}_remove");
let remove_doc = LitStr::new(
&format!("Removes one label set from the `{metric_name}` family."),
Span::call_site(),
);
let clear_ident = format_ident!("{metric_name}_clear");
let clear_doc = LitStr::new(
&format!("Removes all label sets from the `{metric_name}` family."),
Span::call_site(),
);
quote! {
#[doc = #remove_doc]
#(#cfg)*
#fn_vis #fn_token #remove_ident(#(#fn_args,)*) #arrow_token bool {
#convert_args
#foundations::reexports_for_macros::prometools::serde::Family::remove(
&#metrics_struct.#metric_name,
&__args,
)
}
#[doc = #clear_doc]
#(#cfg)*
#fn_vis #fn_token #clear_ident() {
#foundations::reexports_for_macros::prometools::serde::Family::clear(
&#metrics_struct.#metric_name,
)
}
}
} else {
quote! {}
};
quote! {
#[doc = #doc]
#(#cfg)*
#[must_use]
#fn_vis #fn_token #metric_name(#(#fn_args,)*) #arrow_token #metric_type {
#convert_args
#access_metric
}
#removal_fns
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::test_utils::{code_str, parse_attr};
use crate::metrics::validation;
use syn::{ExprStruct, parse_quote};
#[test]
fn expand_empty() {
let attr = parse_attr! {
#[metrics]
};
let src = parse_quote! {
#[attr]
mod empty {}
};
let actual = expand_from_parsed(attr, src).to_string();
let expected = code_str! {
#[attr]
mod empty {
use super::*;
#[allow(non_camel_case_types)]
struct __empty_Metrics {}
#[allow(non_upper_case_globals)]
static __empty_Metrics: ::std::sync::LazyLock<__empty_Metrics> =
::std::sync::LazyLock::new(|| { __empty_Metrics {} });
}
};
assert_eq!(actual, expected);
}
#[test]
fn expand_simple() {
let attr = parse_attr! {
#[metrics(crate_path = "tarmac")]
};
let src = parse_quote! {
pub mod oxy {
pub fn connections_total() -> Counter;
}
};
let actual = expand_from_parsed(attr, src).to_string();
let expected = code_str! {
pub mod oxy {
use super::*;
#[allow(non_camel_case_types)]
struct __oxy_Metrics {
connections_total: Counter,
}
#[allow(non_upper_case_globals)]
static __oxy_Metrics: ::std::sync::LazyLock<__oxy_Metrics> =
::std::sync::LazyLock::new(|| {
let registry = &mut *tarmac::telemetry::metrics::internal::Registries::get_subsystem(stringify!(oxy), false, true);
__oxy_Metrics {
connections_total: {
let metric = ::std::default::Default::default();
tarmac::reexports_for_macros::prometheus_client::registry::Registry::register(
registry,
::std::stringify!(connections_total),
str::trim(" Total number of connections"),
::std::boxed::Box::new(::std::clone::Clone::clone(&metric))
);
metric
},
}
});
#[doc = " Total number of connections"]
#[must_use]
pub fn connections_total() -> Counter {
::std::clone::Clone::clone(&__oxy_Metrics.connections_total)
}
}
};
assert_eq!(actual, expected);
}
#[test]
fn expand_simple_optional_only() {
let attr = parse_attr! {
#[metrics]
};
let src = parse_quote! {
pub(crate) mod oxy {
#[optional]
pub(crate) fn connections_total() -> Counter;
}
};
let actual = expand_from_parsed(attr, src).to_string();
let expected = code_str! {
pub(crate) mod oxy {
use super::*;
#[allow(non_camel_case_types)]
struct __oxy_Metrics {
connections_total: Counter,
}
#[allow(non_upper_case_globals)]
static __oxy_Metrics: ::std::sync::LazyLock<__oxy_Metrics> =
::std::sync::LazyLock::new(|| {
let opt_registry = &mut *::foundations::telemetry::metrics::internal::Registries::get_subsystem(stringify!(oxy), true, true);
__oxy_Metrics {
connections_total: {
let metric = ::std::default::Default::default();
::foundations::reexports_for_macros::prometheus_client::registry::Registry::register(
opt_registry,
::std::stringify!(connections_total),
str::trim(" Total number of connections"),
::std::boxed::Box::new(::std::clone::Clone::clone(&metric))
);
metric
},
}
});
#[doc = " Total number of connections"]
#[must_use]
pub(crate) fn connections_total() -> Counter {
::std::clone::Clone::clone(&__oxy_Metrics.connections_total)
}
}
};
assert_eq!(actual, expected);
}
#[test]
fn expand_unprefixed_mixed() {
let attr = parse_attr! {
#[metrics(unprefixed)]
};
let src = parse_quote! {
pub(crate) mod oxy {
pub(crate) fn requests_total() -> Counter;
#[optional]
pub(crate) fn connections_total() -> Counter;
}
};
let actual = expand_from_parsed(attr, src).to_string();
let expected = code_str! {
pub(crate) mod oxy {
use super::*;
#[allow(non_camel_case_types)]
struct __oxy_Metrics {
requests_total: Counter,
connections_total: Counter,
}
#[allow(non_upper_case_globals)]
static __oxy_Metrics: ::std::sync::LazyLock<__oxy_Metrics> =
::std::sync::LazyLock::new(|| {
let registry = &mut *::foundations::telemetry::metrics::internal::Registries::get_subsystem(stringify!(oxy), false, false);
let opt_registry = &mut *::foundations::telemetry::metrics::internal::Registries::get_subsystem(stringify!(oxy), true, false);
__oxy_Metrics {
requests_total: {
let metric = ::std::default::Default::default();
::foundations::reexports_for_macros::prometheus_client::registry::Registry::register(
registry,
::std::stringify!(requests_total),
str::trim(" Total number of requests"),
::std::boxed::Box::new(::std::clone::Clone::clone(&metric))
);
metric
},
connections_total: {
let metric = ::std::default::Default::default();
::foundations::reexports_for_macros::prometheus_client::registry::Registry::register(
opt_registry,
::std::stringify!(connections_total),
str::trim(" Total number of connections"),
::std::boxed::Box::new(::std::clone::Clone::clone(&metric))
);
metric
},
}
});
#[doc = " Total number of requests"]
#[must_use]
pub(crate) fn requests_total() -> Counter {
::std::clone::Clone::clone(&__oxy_Metrics.requests_total)
}
#[doc = " Total number of connections"]
#[must_use]
pub(crate) fn connections_total() -> Counter {
::std::clone::Clone::clone(&__oxy_Metrics.connections_total)
}
}
};
assert_eq!(actual, expected);
}
#[test]
fn expand_label_set() {
let attr = parse_attr! {
#[metrics]
};
let src = parse_quote! {
pub mod oxy {
pub fn connections_errors_total(
#[serde(rename = "endpoint_name")]
endpoint: &Arc<String>,
#[serde_as(as = "DisplayFromStr")]
kind: Kind,
message: &'static str,
error: impl Into<String>,
) -> Counter;
}
};
let actual = expand_from_parsed(attr, src).to_string();
let expected = code_str! {
pub mod oxy {
use super::*;
#[allow(non_camel_case_types)]
struct __oxy_Metrics {
connections_errors_total:
::foundations::reexports_for_macros::prometools::serde::Family<
connections_errors_total,
Counter,
>,
}
#[allow(non_camel_case_types)]
#[::foundations::reexports_for_macros::serde_with::serde_as(crate = ":: foundations :: reexports_for_macros :: serde_with")]
#[derive(
::std::clone::Clone,
::std::cmp::Eq,
::std::hash::Hash,
::std::cmp::PartialEq,
::foundations::reexports_for_macros::serde::Serialize,
)]
#[serde(crate = ":: foundations :: reexports_for_macros :: serde")]
struct connections_errors_total {
#[serde(rename = "endpoint_name")]
endpoint: Arc<String>,
#[serde_as(as = "DisplayFromStr")]
kind: Kind,
message: &'static str,
error: String,
}
#[allow(non_upper_case_globals)]
static __oxy_Metrics: ::std::sync::LazyLock<__oxy_Metrics> =
::std::sync::LazyLock::new(|| {
let registry = &mut *::foundations::telemetry::metrics::internal::Registries::get_subsystem(stringify!(oxy), false, true);
__oxy_Metrics {
connections_errors_total: {
let metric = ::std::default::Default::default();
::foundations::reexports_for_macros::prometheus_client::registry::Registry::register(
registry,
::std::stringify!(connections_errors_total),
str::trim(" Total number of connection errors"),
::std::boxed::Box::new(::std::clone::Clone::clone(&metric))
);
metric
},
}
});
#[doc = " Total number of connection errors"]
#[must_use]
pub fn connections_errors_total(
endpoint: &Arc<String>,
kind: Kind,
message: &'static str,
error: impl Into<String>,
) -> Counter {
let __args = connections_errors_total {
endpoint: ::std::clone::Clone::clone(endpoint),
kind,
message,
error: ::std::convert::Into::into(error),
};
::std::clone::Clone::clone(
&::foundations::reexports_for_macros::prometools::serde::Family::get_or_create(
&__oxy_Metrics.connections_errors_total,
&__args,
)
)
}
}
};
assert_eq!(actual, expected);
}
#[test]
fn expand_ctor() {
let attr = parse_attr! {
#[metrics]
};
let src = parse_quote! {
pub mod oxy {
#[ctor = HistogramBuilder { buckets: &[0.5, 1.] }]
pub fn connections_latency() -> Histogram;
#[ctor = HistogramBuilder { buckets: &[2., 3.] }]
pub fn requests_per_connection(endpoint: String) -> Histogram;
}
};
let actual = expand_from_parsed(attr, src).to_string();
let expected = code_str! {
pub mod oxy {
use super::*;
#[allow(non_camel_case_types)]
struct __oxy_Metrics {
connections_latency: Histogram,
requests_per_connection:
::foundations::reexports_for_macros::prometools::serde::Family<
requests_per_connection,
Histogram,
HistogramBuilder,
>,
}
#[allow(non_camel_case_types)]
#[derive(
::std::clone::Clone,
::std::cmp::Eq,
::std::hash::Hash,
::std::cmp::PartialEq,
::foundations::reexports_for_macros::serde::Serialize,
)]
#[serde(crate = ":: foundations :: reexports_for_macros :: serde")]
struct requests_per_connection {
endpoint: String,
}
#[allow(non_upper_case_globals)]
static __oxy_Metrics: ::std::sync::LazyLock<__oxy_Metrics> =
::std::sync::LazyLock::new(|| {
let registry = &mut *::foundations::telemetry::metrics::internal::Registries::get_subsystem(stringify!(oxy), false, true);
__oxy_Metrics {
connections_latency: {
let metric = ::foundations::reexports_for_macros::prometheus_client::metrics::family::MetricConstructor::new_metric(
&(HistogramBuilder { buckets: &[0.5, 1.] })
);
::foundations::reexports_for_macros::prometheus_client::registry::Registry::register(
registry,
::std::stringify!(connections_latency),
str::trim(" Latency of connections"),
::std::boxed::Box::new(::std::clone::Clone::clone(&metric))
);
metric
},
requests_per_connection: {
let metric = ::foundations::reexports_for_macros::prometools::serde::Family::new_with_constructor(
HistogramBuilder { buckets: &[2., 3.] }
);
::foundations::reexports_for_macros::prometheus_client::registry::Registry::register(
registry,
::std::stringify!(requests_per_connection),
str::trim(" Number of requests per connection"),
::std::boxed::Box::new(::std::clone::Clone::clone(&metric))
);
metric
},
}
});
#[doc = " Latency of connections"]
#[must_use]
pub fn connections_latency() -> Histogram {
::std::clone::Clone::clone(&__oxy_Metrics.connections_latency)
}
#[doc = " Number of requests per connection"]
#[must_use]
pub fn requests_per_connection(
endpoint: String,
) -> Histogram {
let __args = requests_per_connection { endpoint, };
::std::clone::Clone::clone(
&::foundations::reexports_for_macros::prometools::serde::Family::get_or_create(
&__oxy_Metrics.requests_per_connection,
&__args,
)
)
}
}
};
assert_eq!(actual, expected);
}
#[cfg(foundations_unstable)]
#[test]
fn expand_with_removal() {
let attr = parse_attr! {
#[metrics]
};
let src = parse_quote! {
pub(crate) mod oxy {
#[with_removal]
pub(crate) fn requests_total(status: u16) -> Counter;
}
};
let actual = expand_from_parsed(attr, src).to_string();
let expected = code_str! {
pub(crate) mod oxy {
use super::*;
#[allow(non_camel_case_types)]
struct __oxy_Metrics {
requests_total:
::foundations::reexports_for_macros::prometools::serde::Family<
requests_total,
Counter,
>,
}
#[allow(non_camel_case_types)]
#[derive(
::std::clone::Clone,
::std::cmp::Eq,
::std::hash::Hash,
::std::cmp::PartialEq,
::foundations::reexports_for_macros::serde::Serialize,
)]
#[serde(crate = ":: foundations :: reexports_for_macros :: serde")]
struct requests_total {
status: u16,
}
#[allow(non_upper_case_globals)]
static __oxy_Metrics: ::std::sync::LazyLock<__oxy_Metrics> =
::std::sync::LazyLock::new(|| {
let registry = &mut *::foundations::telemetry::metrics::internal::Registries::get_subsystem(stringify!(oxy), false, true);
__oxy_Metrics {
requests_total: {
let metric = ::std::default::Default::default();
::foundations::reexports_for_macros::prometheus_client::registry::Registry::register(
registry,
::std::stringify!(requests_total),
str::trim(" Total number of requests"),
::std::boxed::Box::new(::std::clone::Clone::clone(&metric))
);
metric
},
}
});
#[doc = " Total number of requests"]
#[must_use]
pub(crate) fn requests_total(status: u16,) -> Counter {
let __args = requests_total { status, };
::std::clone::Clone::clone(
&::foundations::reexports_for_macros::prometools::serde::Family::get_or_create(
&__oxy_Metrics.requests_total,
&__args,
)
)
}
#[doc = "Removes one label set from the `requests_total` family."]
pub(crate) fn requests_total_remove(status: u16,) -> bool {
let __args = requests_total { status, };
::foundations::reexports_for_macros::prometools::serde::Family::remove(
&__oxy_Metrics.requests_total,
&__args,
)
}
#[doc = "Removes all label sets from the `requests_total` family."]
pub(crate) fn requests_total_clear() {
::foundations::reexports_for_macros::prometools::serde::Family::clear(
&__oxy_Metrics.requests_total,
)
}
}
};
assert_eq!(actual, expected);
}
#[test]
fn test_valid_histogram_buckets() {
let expr: ExprStruct = parse_quote! {
HistogramBuilder { buckets: &[0.1, 0.2, 0.3, 0.4] }
};
assert!(validation::validate_histogram_buckets(&expr).is_ok());
}
#[test]
fn test_empty_histogram_buckets() {
let expr: ExprStruct = parse_quote! {
HistogramBuilder { buckets: &[] }
};
assert!(validation::validate_histogram_buckets(&expr).is_ok());
}
#[test]
fn test_invalid_histogram_buckets_equal_values() {
let expr: ExprStruct = parse_quote! {
HistogramBuilder { buckets: &[0.1, 0.2, 0.2, 0.3] }
};
assert!(validation::validate_histogram_buckets(&expr).is_err());
}
#[test]
fn test_invalid_histogram_buckets_decreasing_values() {
let expr: ExprStruct = parse_quote! {
HistogramBuilder { buckets: &[0.3, 0.2, 0.1] }
};
assert!(validation::validate_histogram_buckets(&expr).is_err());
}
#[test]
fn test_skip_validation_for_variable_buckets() {
let expr: ExprStruct = parse_quote! {
HistogramBuilder { buckets: MY_BUCKETS }
};
assert!(validation::validate_histogram_buckets(&expr).is_ok());
}
#[test]
fn test_skip_validation_for_function_buckets() {
let expr: ExprStruct = parse_quote! {
HistogramBuilder { buckets: get_buckets() }
};
assert!(validation::validate_histogram_buckets(&expr).is_ok());
}
}