Skip to main content

metrique_macro/
lib.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4#![deny(missing_docs)]
5#![doc = include_str!("../README.md")]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8mod aggregate;
9mod derive_utils;
10mod emf;
11mod entry_impl;
12mod enums;
13mod inflect;
14mod structs;
15mod value_impl;
16
17use darling::{
18    FromField, FromMeta,
19    ast::NestedMeta,
20    util::{Flag, SpannedValue},
21};
22use emf::DimensionSets;
23use inflect::NameStyle;
24use proc_macro::TokenStream;
25use proc_macro2::{Span, TokenStream as Ts2};
26use quote::{ToTokens, quote, quote_spanned};
27use syn::{
28    Attribute, Data, DeriveInput, Error, Fields, GenericParam, Generics, Ident, Result, Type,
29    Visibility, parse_macro_input, spanned::Spanned,
30};
31
32use crate::inflect::{name_contains_dot, name_contains_uninflectables, name_ends_with_delimiter};
33
34/// Transforms a struct or enum into a wide event (metric record).
35///
36/// # Container Attributes
37///
38/// | Attribute | Type | Description | Example |
39/// |-----------|------|-------------|---------|
40/// | `rename_all` | String | Changes the case style of all field names | `#[metrics(rename_all = "PascalCase")]` |
41/// | `prefix` | String | Adds a prefix to all field names (prefix gets inflected) | `#[metrics(prefix = "api_")]` |
42/// | `exact_prefix` | String | Adds a prefix to all field names without inflection | `#[metrics(exact_prefix = "API_")]` |
43/// | `emf::dimension_sets` | Array | Defines dimension sets for CloudWatch metrics | `#[metrics(emf::dimension_sets = [["Status", "Operation"]])]` |
44/// | `tag` | Nested | On entry enums, adds a tag field with the variant name. Tag value respects `rename_all` and variant `name`, but not `prefix`. | |
45/// | - `name` | String | Name of the tag field (inflectable, respects `prefix` and `rename_all`) | `#[metrics(tag(name = "operation"))]` |
46/// | - `name_exact` | String | Name of the tag field (exact, not affected by `prefix` or `rename_all`) | `#[metrics(tag(name_exact = "operation"))]` |
47/// | - `sample_group` | Flag | Include tag in sample group | `#[metrics(tag(name = "op", sample_group))]` |
48/// | `subfield` | Flag | When set, this metric can only be used when nested within other metrics, and can be consumed by reference (has both `impl CloseValue for &MyStruct` and `impl CloseValue for MyStruct`). It cannot be added to a sink directly. | `#[metrics(subfield)]` |
49/// | `subfield_owned` | Flag | When set, this metric can only be used when nested within other metrics. It cannot be added to a sink directly. | `#[metrics(subfield_owned)]` |
50/// | `value` | Flag | Used for *structs*. Makes the struct a value newtype | `#[metrics(value)]` |
51/// | `value(string)` | Flag | Used for *enums*. Transforms the enum into a string value. Automatically derives `Debug`, `Clone`, and `Copy` on the generated Value enum. The base enum is left untouched — derive what you need on it yourself. | `#[metrics(value(string))]` |
52/// | `sample_group` | Flag | On `#[metrics(value)]`, forwards `sample_group` to the inner field | `#[metrics(value, sample_group)]` |
53///
54/// # Field Attributes
55///
56/// | Attribute | Type | Description | Example |
57/// |-----------|------|-------------|---------|
58/// | `name` | String | Overrides the field name in metrics | `#[metrics(name = "CustomName")]` |
59/// | `unit` | Path | Specifies the unit for the metric value | `#[metrics(unit = Millisecond)]` |
60/// | `format` | Path | Specifies the formatter (`ValueFormatter`) for the metric value | `#[metrics(format=EpochSeconds)]` |
61/// | `timestamp` | Flag | Marks a field as the canonical timestamp | `#[metrics(timestamp)]` |
62/// | `sample_group` | Flag | Marks a field as a sample group - it will still be emitted as a value | `#[metrics(sample_group)]` |
63/// | `prefix` | String | Adds a prefix to flattened entries. Prefix will get inflected to the right case style | `#[metrics(flatten, prefix="prefix-")]` |
64/// | `exact_prefix` | String | Adds a prefix to flattened entries without inflection | `#[metrics(flatten, exact_prefix="API_")]` |
65/// | `flatten` | Flag | Flattens nested `CloseEntry` metric structs | `#[metrics(flatten)]` |
66/// | `flatten_entry` | Flag | Flattens nested `CloseValue<Closed: Entry>` metric structs, with no prefix or inflection | `#[metrics(flatten_entry)]` |
67/// | `no_close` | Flag | Use the entry directly instead of closing it | `#[metrics(no_close)]` |
68/// | `ignore` | Flag | Excludes the field from metrics | `#[metrics(ignore)]` |
69///
70/// # Variant Attributes
71///
72/// For enum usage, see the [Enums](#enums) section below.
73///
74/// | Attribute | Type | Description | Example |
75/// |-----------|------|-------------|---------|
76/// | `name` | String | Overrides the variant name in metrics | `#[metrics(name = "CustomName")]` |
77///
78/// # Metric Names
79///
80/// ## Prefixes
81///
82/// Prefixes can be attached to metrics in 2 different ways:
83///
84/// 1. Prefixes on flattened subfields, which affect all the metrics contained within
85///    the flattened subfield:
86///
87///    ```rust
88///    # use metrique::unit_of_work::metrics;
89///    # use std::time::Duration;
90///    #[metrics(subfield)]
91///    struct Subfield {
92///        request_latency: Duration, // inflected
93///        #[metrics(name="NDucks")] // not inflected (since `name` is not inflected), prefixed
94///        number_of_ducks: u32,
95///    }
96///
97///    #[metrics(rename_all = "kebab-case")]
98///    struct Base {
99///        // uses `exact_prefix`, not inflected
100///        #[metrics(flatten, exact_prefix = "API:")]
101///        api: Subfield,
102///        // uses `prefix`, inflected
103///        #[metrics(flatten, prefix = "alt")]
104///        alt: Subfield,
105///    }
106///
107///    let vec_sink = metrique::writer::sink::VecEntrySink::new();
108///    Base {
109///        api: Subfield { request_latency: Duration::from_millis(1), number_of_ducks: 0 },
110///        alt: Subfield { request_latency: Duration::from_millis(1), number_of_ducks: 0 }
111///    }.append_on_drop(vec_sink.clone());
112///    let entries = vec_sink.drain();
113///    let entry = metrique::test_util::to_test_entry(&entries[0]);
114///    assert_eq!(entry.metrics["API:request-latency"], 1.0);
115///    assert_eq!(entry.metrics["alt-request-latency"], 1.0);
116///    assert_eq!(entry.metrics["API:NDucks"], 0);
117///    assert_eq!(entry.metrics["alt-NDucks"], 0);
118///    ```
119/// 2. Prefixes on the struct itself, which *only* affect fields within the metric
120///    that don't have a `name` or a `flatten` attribute:
121///
122///    ```rust
123///    # use metrique::unit_of_work::metrics;
124///    # use std::time::Duration;
125///    #[metrics(subfield)]
126///    struct Subfield {
127///        request_latency: Duration, // inflected
128///    }
129///
130///    #[metrics(prefix = "Foo-" /* prefix gets inflected */, rename_all = "kebab-case")]
131///    struct Base {
132///        // prefix does not propagate to subfield. Use `prefix = "Foo-"` to propagate
133///        #[metrics(flatten)]
134///        sub: Subfield,
135///        // prefix does not propagate to named field
136///        #[metrics(name = "n-ducks")]
137///        number_of_ducks: u32,
138///        // prefix does propagate to other
139///        number_of_geese: u32,
140///    }
141///
142///    let vec_sink = metrique::writer::sink::VecEntrySink::new();
143///    Base {
144///        sub: Subfield { request_latency: Duration::from_millis(1) },
145///        number_of_ducks: 0,
146///        number_of_geese: 0
147///    }.append_on_drop(vec_sink.clone());
148///    let entries = vec_sink.drain();
149///    let entry = metrique::test_util::to_test_entry(&entries[0]);
150///    assert_eq!(entry.metrics["request-latency"], 1.0);
151///    assert_eq!(entry.metrics["n-ducks"], 0);
152///    // prefix-on-struct only applies to this
153///    assert_eq!(entry.metrics["foo-number-of-geese"], 0);
154///    ```
155///
156/// Note that prefix-attribute-on-flatten *does* apply to nested fields that have
157/// a `name` attribute.
158///
159/// Prefixes can either be inflectable (with the `prefix` attribute) or non-inflectable
160/// (with the `exact_prefix` attribute).
161///
162/// ## Inflection
163///
164/// Metric names are inflected to allow them to fit into the name style used by the
165/// application. This uses the `Inflector` crate and supports inflecting metrics into
166/// PascalCase, snake_case, and kebab-case.
167///
168/// Metric names assigned via the `name` attribute are not inflected, but if they are
169/// contained in a metric with a prefix, the prefix can be inflected. Prefixes assigned via
170/// `exact_prefix` are similarly not inflected.
171///
172/// For example, this emits a metric named "foo_Bar", since "Bar" is assigned via a
173/// `name` attribute and therefore not inflected, but the prefix is assigned
174/// via `prefix` and is therefore inflected.
175///
176/// ```rust
177/// # use metrique::unit_of_work::metrics;
178///
179/// #[metrics(subfield)]
180/// struct Subfield {
181///     #[metrics(name = "NDucks")]
182///     number_of_ducks: u32,
183/// }
184///
185/// #[metrics(rename_all = "snake_case")]
186/// struct Base {
187///     #[metrics(flatten, prefix = "Waterfowl_")]
188///     waterfowl: Subfield,
189/// }
190///
191/// let vec_sink = metrique::writer::sink::VecEntrySink::new();
192/// Base { waterfowl: Subfield { number_of_ducks: 0 } }
193///     .append_on_drop(vec_sink.clone());
194/// let entries = vec_sink.drain();
195/// let entry = metrique::test_util::to_test_entry(&entries[0]);
196/// assert_eq!(entry.metrics["waterfowl_NDucks"], 0);
197/// ```
198///
199/// # Example
200///
201/// ```rust
202/// use metrique::unit_of_work::metrics;
203/// use metrique::timers::{Timestamp, Timer};
204/// use metrique::unit::{Count, Millisecond};
205/// use metrique::writer::GlobalEntrySink;
206/// use metrique::ServiceMetrics;
207/// use std::time::SystemTime;
208///
209/// #[metrics(value(string), rename_all = "snake_case")]
210/// enum Operation {
211///    CountDucks
212/// }
213///
214/// #[metrics(value)]
215/// struct RequestCount(#[metrics(unit=Count)] usize);
216///
217/// #[metrics(rename_all = "PascalCase")]
218/// struct RequestMetrics {
219///     #[metrics(sample_group)]
220///     operation: Operation,
221///
222///     #[metrics(timestamp)]
223///     timestamp: SystemTime,
224///
225///     #[metrics(unit = Millisecond)]
226///     operation_time: Timer,
227///
228///     #[metrics(flatten, prefix = "sub_")]
229///     nested: NestedMetrics,
230///
231///     request_count: RequestCount,
232/// }
233///
234/// #[metrics(subfield)]
235/// struct NestedMetrics {
236///     #[metrics(name = "CustomCounter")]
237///     counter: usize,
238/// }
239///
240/// impl RequestMetrics {
241///     fn init(operation: Operation) -> RequestMetricsGuard {
242///         RequestMetrics {
243///             timestamp: SystemTime::now(),
244///             operation,
245///             operation_time: Timer::start_now(),
246///             nested: NestedMetrics { counter: 0 },
247///             request_count: RequestCount(0),
248///         }.append_on_drop(ServiceMetrics::sink())
249///     }
250/// }
251/// ```
252/// # Enums
253///
254/// Enums can be used in two ways: as value enums or entry enums.
255///
256/// ## Value Enums
257///
258/// Value enums with `#[metrics(value(string))]` convert enum variants to string values.
259/// Only unit variants are allowed. Variant names respect `#[metrics(name = "...")]` and `rename_all`.
260///
261/// `Debug`, `Clone`, and `Copy` are automatically derived on the generated Value enum.
262/// The base enum is not modified — add your own derives as needed:
263///
264/// ```rust
265/// # use metrique::unit_of_work::metrics;
266/// #[metrics(value(string), rename_all = "snake_case")]
267/// enum Operation {
268///     #[metrics(name = "custom_read")]
269///     ReadData,
270///     WriteData,
271/// }
272/// // Operation::ReadData converts to "custom_read"
273/// // Operation::WriteData converts to "write_data"
274///
275/// #[metrics]
276/// struct Request {
277///     #[metrics(sample_group)]
278///     operation: Operation,
279///     count: usize,
280/// }
281/// ```
282///
283/// ## Entry Enums
284///
285/// Entry enums allow different metric fields per variant. Contained fields respect container and
286/// field attributes as used by structs.
287///
288/// Variants can be tuple variants (which must use flatten/flatten_entry/ignore attributes, since
289/// their fields are unnamed). Or, they can use struct variants with named fields and the full
290/// range of field attributes available. (Unit variants are also supported but don't do much unless
291/// used with a `tag` field; see the following `Tag field` section.)
292///
293/// ```rust
294/// # use metrique::unit_of_work::metrics;
295/// # use metrique::unit::Millisecond;
296/// # use std::time::Duration;
297///
298/// #[metrics(subfield)]
299/// struct ReadMetrics {
300///     bytes_read: usize,
301/// }
302///
303/// #[metrics(rename_all = "PascalCase")]
304/// enum Operation {
305///     Read(#[metrics(flatten)] ReadMetrics),
306///     Write {
307///         #[metrics(unit = Millisecond)]
308///         latency: Duration,
309///         bytes_written: usize,
310///     },
311/// }
312///
313/// let entry = metrique::test_util::test_metric(
314///     Operation::Read(ReadMetrics { bytes_read: 1024 })
315/// );
316/// assert_eq!(entry.metrics["BytesRead"], 1024);
317///
318/// let entry = metrique::test_util::test_metric(
319///     Operation::Write { latency: Duration::from_millis(5), bytes_written: 2048 }
320/// );
321/// assert_eq!(entry.metrics["Latency"], 5);
322/// assert_eq!(entry.metrics["BytesWritten"], 2048);
323/// ```
324///
325/// ### Tag Field
326///
327/// Entry enums can include a `tag` attribute to add a field containing the variant name:
328///
329/// ```rust
330/// # use metrique::unit_of_work::metrics;
331/// # use metrique::test_util::test_metric;
332/// # use metrique::unit::Millisecond;
333/// # use std::time::Duration;
334///
335/// #[metrics(tag(name = "Operation"), rename_all = "PascalCase")]
336/// enum Request {
337///     Read { bytes: usize },
338///     Write {
339///         #[metrics(unit = Millisecond)]
340///         latency: Duration,
341///     },
342///     // doesn't contain fields, but still has the tag field injected
343///     Delete,
344/// }
345///
346/// let entry = test_metric(Request::Read { bytes: 1024 });
347/// assert_eq!(entry.values["Operation"], "Read");  // Tag field with variant name
348/// assert_eq!(entry.metrics["Bytes"], 1024);
349/// ```
350///
351/// The tag field name is specified explicitly and not affected by `prefix` or `rename_all`.
352/// The tag value (variant name) respects `rename_all` and variant `name` attributes, but not `prefix`.
353///
354/// The optional `sample_group` flag includes the tag field in the sample group:
355///
356/// ```rust
357/// # use metrique::unit_of_work::metrics;
358/// # use metrique::test_util::test_metric;
359///
360/// #[metrics(tag(name = "Operation", sample_group))]
361/// enum Request {
362///     Read { bytes: usize },
363///     Write { bytes: usize },
364/// }
365///
366/// let entry = test_metric(Request::Read { bytes: 1024 });
367/// // The tag field "Operation" with value "Read" is included in sample_group
368/// ```
369///
370/// # Generated Types
371///
372/// For a struct or entry enum named `MyMetrics`, the macro generates:
373/// - `MyMetricsEntry`: The internal representation used for serialization, implements `InflectableEntry`
374/// - `MyMetricsGuard`: A wrapper that implements `Deref`/`DerefMut` to the original struct and handles emission on drop.
375///   A type alias to ``AppendAndCloseOnDrop`.
376/// - `MyMetricsHandle`: A shareable handle for concurrent access to the metrics.
377///   A type alias to ``AppendAndCloseOnDropHandle`.
378///
379/// Value enums do not have new types generated, only trait implementations (`From<&MyEnum> for &'static str`, `SampleGroup`, `Value`).
380#[proc_macro_attribute]
381pub fn metrics(attr: TokenStream, input: proc_macro::TokenStream) -> proc_macro::TokenStream {
382    let input = parse_macro_input!(input as DeriveInput);
383
384    // There's a little bit of juggling here so we can return errors both from the root attribute & the inner attribute.
385    // We will also write the compiler error from the root attribute into the token stream if it failed. But if it did fail,
386    // we still analyze the main macro by passing in an empty root attributes instead.
387
388    let mut base_token_stream = Ts2::new();
389    let root_attrs = match parse_root_attrs(attr) {
390        Ok(root_attrs) => root_attrs,
391        Err(e) => {
392            // recover and use an empty root attributes
393            e.to_compile_error().to_tokens(&mut base_token_stream);
394            RootAttributes::default()
395        }
396    };
397
398    // Try to generate the full metrics implementation
399    match generate_metrics(root_attrs, input.clone()) {
400        Ok(output) => output.to_tokens(&mut base_token_stream),
401        Err(err) => {
402            // Always generate the base struct without metrics attributes to avoid cascading errors
403            clean_base_adt(&input).to_tokens(&mut base_token_stream);
404            // Include the error and the base struct without metrics attributes
405            err.to_compile_error().to_tokens(&mut base_token_stream);
406        }
407    };
408    base_token_stream.into()
409}
410
411/// Generates aggregation support for metrics structs.
412///
413/// This macro enables combining multiple observations of the same metric into a single aggregated
414/// entry. It must be placed before `#[metrics]` and generates:
415/// - An `Aggregated{StructName}` struct with aggregated field types
416/// - An `AggregateEntry` trait implementation for merging observations
417///
418/// For renaming fields and adding unit, `#[aggregate]` relies on the `#[metrics]` macro.
419///
420/// # Container Attributes
421///
422/// | Attribute | Type | Description | Example |
423/// |-----------|------|-------------|---------|
424/// | `direct` | Flag | Aggregates on the struct itself instead of the closed entry (default: aggregates on closed entry) | `#[aggregate(direct)]` |
425///
426/// # Field Attributes
427///
428/// | Attribute | Type | Description | Example |
429/// |-----------|------|-------------|---------|
430/// | `strategy` | Path | Specifies the aggregation strategy (required for non-key fields) | `#[aggregate(strategy = Histogram<Duration>)]` |
431/// | `key` | Flag | Marks a field as part of the aggregation key - observations with different keys are aggregated separately | `#[aggregate(key)]` |
432/// | `ignore` | Flag | Ignore a field during aggregation. The field will still be part of the non-aggregated metric entry unless also marked `#[metrics(ignore)`] |
433///
434/// # Aggregation Modes
435///
436/// ## Entry Mode (Default)
437///
438/// By default, `#[aggregate]` implements aggregation on the closed metric entry. This means
439/// aggregation happens after `CloseValue` has been applied to all fields:
440///
441/// ```
442/// use metrique::unit_of_work::metrics;
443/// use metrique_aggregation::{aggregate, histogram::Histogram, value::Sum};
444/// use std::time::Duration;
445///
446/// #[aggregate]
447/// #[metrics]
448/// struct ApiCall {
449///     #[aggregate(strategy = Histogram<Duration>)]
450///     #[metrics(unit = metrique::writer::unit::Millisecond)]
451///     latency: Duration,  // Aggregates Duration values
452///
453///     #[aggregate(strategy = Sum)]
454///     response_size: usize,
455/// }
456/// ```
457///
458/// ## Direct Mode
459///
460/// Use `#[aggregate(direct)]` to aggregate on the struct iteslf. In direct mode:
461/// - Aggregation strategies receive the raw field type before `CloseValue` is applied
462/// - Use this if the base struct is not also a metric
463///
464/// # Aggregation Keys
465///
466/// Fields marked with `#[aggregate(key)]` define the aggregation key. Observations with different
467/// keys are aggregated into separate entries:
468///
469/// ```
470/// use metrique::unit_of_work::metrics;
471/// use metrique_aggregation::{aggregate, histogram::Histogram};
472/// use std::time::Duration;
473///
474/// #[aggregate]
475/// #[metrics]
476/// struct ApiCall {
477///     #[aggregate(key)]
478///     endpoint: String,  // Separate aggregation per endpoint
479///
480///     #[aggregate(strategy = Histogram<Duration>)]
481///     latency: Duration,
482/// }
483/// ```
484///
485/// Key behavior:
486/// - Key fields are cloned into the aggregated struct unchanged
487/// - Multiple key fields create a tuple key: `(Field1, Field2, ...)`
488/// - Without key fields, all observations aggregate into a single entry
489/// - Key fields must implement `Clone`
490///
491/// # Aggregation Strategies
492///
493/// Each non-key field must specify an aggregation strategy that implements `AggregateValue<T>`:
494///
495/// ## Built-in Strategies
496///
497/// - **`Sum`** - Sums numeric values together
498/// - **`Histogram<T>`** - Collects values into a distribution. Histogram has a second generic that can control how values are stored. See the `Histogram` docs for more info.
499/// - **`KeepLast`** - Keeps the most recent value
500///
501/// ```
502/// use metrique::unit_of_work::metrics;
503/// use metrique_aggregation::{aggregate, histogram::Histogram, value::Sum};
504/// use std::time::Duration;
505///
506/// #[aggregate]
507/// #[metrics]
508/// struct ApiCall {
509///     #[aggregate(strategy = Histogram<Duration>)]
510///     latency: Duration,
511///
512///     #[aggregate(strategy = Sum)]
513///     bytes_sent: usize,
514/// }
515/// ```
516///
517/// # Generated Types
518///
519/// For a struct with `#[aggregate]`, the macro generates:
520/// - `AggregatedMyMetrics`: The aggregated struct where each field is replaced with its aggregated type
521/// - `impl AggregateEntry for MyMetrics`: Trait implementation for merging observations
522///
523/// For more details on the aggregation trait system, see the
524/// [traits module documentation](https://docs.rs/metrique-aggregation/latest/metrique_aggregation/traits/index.html).
525///
526/// The aggregated struct can be used with `Aggregate<T>` or `MutexSink<T>`:
527///
528/// ```
529/// use metrique::unit_of_work::metrics;
530/// use metrique_aggregation::{aggregate, histogram::Histogram, aggregator::Aggregate};
531/// use std::time::Duration;
532///
533/// #[aggregate]
534/// #[metrics]
535/// struct ApiCall {
536///     #[aggregate(strategy = Histogram<Duration>)]
537///     latency: Duration,
538/// }
539///
540/// #[metrics]
541/// struct RequestMetrics {
542///     request_id: String,
543///     #[metrics(flatten)]
544///     api_calls: Aggregate<ApiCall>,
545/// }
546/// ```
547///
548/// For more examples, see the `examples` directory in `metrique-aggregation`
549#[proc_macro_attribute]
550pub fn aggregate(attr: TokenStream, input: TokenStream) -> TokenStream {
551    let input = parse_macro_input!(input as DeriveInput);
552    let attr_str = attr.to_string();
553    let entry_mode = attr.is_empty() || attr_str.trim() != "direct";
554    let enable_merge_ref = attr_str.contains("ref");
555
556    let mut output = Ts2::new();
557
558    // Try to generate struct, impl, MergeRef, and merge methods
559    let struct_result = aggregate::generate_aggregated_struct(&input, entry_mode);
560    let impl_result = aggregate::generate_aggregate_strategy_impl(&input, entry_mode);
561    let merge_ref_result = aggregate::generate_merge_ref_impl(&input, entry_mode, enable_merge_ref);
562    let merge_methods_result = aggregate::generate_merge_on_drop_methods(&input, entry_mode);
563
564    match (
565        struct_result,
566        impl_result,
567        merge_ref_result,
568        merge_methods_result,
569    ) {
570        (Ok(aggregated_struct), Ok(aggregate_impl), Ok(merge_ref_impl), Ok(merge_methods)) => {
571            aggregated_struct.to_tokens(&mut output);
572            aggregate_impl.to_tokens(&mut output);
573            if let Some(merge_ref) = merge_ref_impl {
574                merge_ref.to_tokens(&mut output);
575            }
576            merge_methods.to_tokens(&mut output);
577            aggregate::clean_aggregate_adt(&input).to_tokens(&mut output);
578        }
579        (Err(e), _, _, _) | (_, Err(e), _, _) | (_, _, Err(e), _) | (_, _, _, Err(e)) => {
580            // On error, generate the base struct without aggregate attributes and include the error
581            aggregate::clean_aggregate_adt(&input).to_tokens(&mut output);
582            e.to_compile_error().to_tokens(&mut output);
583        }
584    }
585
586    output.into()
587}
588
589#[derive(Copy, Clone, Debug)]
590enum OwnershipKind {
591    ByRef,
592    ByValue,
593}
594
595#[derive(Debug, Default, FromMeta)]
596// allow both `#[metric(value)]` and `#[metric(value(string))]` to be parsed
597#[darling(from_word = Self::from_word)]
598struct ValueAttributes {
599    string: Flag,
600}
601
602impl ValueAttributes {
603    /// constructor used in case of the `#[metric(value)]` form
604    fn from_word() -> darling::Result<Self> {
605        Ok(Self::default())
606    }
607}
608
609/// Synthetic field using variant name.
610#[derive(Debug, Clone)]
611pub(crate) enum Tag {
612    Inflectable { name: String, sample_group: bool },
613    Exact { name: String, sample_group: bool },
614}
615
616impl Tag {
617    /// Get the tag field name, applying inflection if using inflectable variant
618    pub(crate) fn field_name(&self, root_attrs: &RootAttributes) -> String {
619        match self {
620            Tag::Inflectable { name, .. } => root_attrs
621                .prefix
622                .as_ref()
623                .map(|p| p.apply(name, root_attrs.rename_all))
624                .unwrap_or_else(|| root_attrs.rename_all.apply(name)),
625            Tag::Exact { name, .. } => name.clone(),
626        }
627    }
628
629    pub(crate) fn sample_group(&self) -> bool {
630        match self {
631            Tag::Inflectable { sample_group, .. } => *sample_group,
632            Tag::Exact { sample_group, .. } => *sample_group,
633        }
634    }
635}
636
637#[derive(Debug, FromMeta)]
638#[darling(and_then = Self::validate, from_word = Self::from_word)]
639struct RawTag {
640    #[darling(default)]
641    name: Option<SpannedKv<String>>,
642    #[darling(default)]
643    name_exact: Option<SpannedKv<String>>,
644    #[darling(default)]
645    sample_group: Flag,
646}
647
648impl RawTag {
649    fn from_word() -> darling::Result<Self> {
650        Err(darling::Error::custom(
651            "tag requires either name or name_exact parameter: #[metrics(tag(name = \"...\"))] or #[metrics(tag(name_exact = \"...\"))]",
652        ))
653    }
654
655    fn validate(self) -> darling::Result<Self> {
656        match (self.name, self.name_exact) {
657            (None, None) => Err(darling::Error::custom(
658                "tag requires either name or name_exact parameter: #[metrics(tag(name = \"...\"))] or #[metrics(tag(name_exact = \"...\"))]",
659            )),
660            (Some(_), Some(_)) => Err(darling::Error::custom(
661                "tag cannot have both name and name_exact parameters",
662            )),
663            (Some(name), None) => Ok(Self {
664                name: Some(validate_name(name)?),
665                name_exact: None,
666                sample_group: self.sample_group,
667            }),
668            (None, Some(name_exact)) => Ok(Self {
669                name: None,
670                name_exact: Some(validate_name(name_exact)?),
671                sample_group: self.sample_group,
672            }),
673        }
674    }
675}
676
677impl From<RawTag> for Tag {
678    fn from(raw: RawTag) -> Self {
679        let sample_group = raw.sample_group.is_present();
680        match (raw.name, raw.name_exact) {
681            (Some(name), None) => Tag::Inflectable {
682                name: name.value,
683                sample_group,
684            },
685            (None, Some(name)) => Tag::Exact {
686                name: name.value,
687                sample_group,
688            },
689            _ => unreachable!("validated in RawTag::validate"),
690        }
691    }
692}
693
694/// Wrapper for parsing `flags(Path1, Path2, ...)` as a single darling field.
695///
696/// Implements `FromMeta` by parsing the token stream directly via `parse_args_with`.
697/// This works around a darling limitation where custom `FromMeta` types are silently
698/// dropped when they appear alongside darling `Flag` fields (like `flatten`, `no_close`)
699/// in the same attribute struct.
700#[derive(Debug, Clone, Default)]
701pub(crate) struct FlagsList(pub(crate) Vec<syn::Path>);
702
703impl FromMeta for FlagsList {
704    fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
705        match item {
706            syn::Meta::List(list) => {
707                let parsed: syn::punctuated::Punctuated<syn::Path, syn::Token![,]> = list
708                    .parse_args_with(syn::punctuated::Punctuated::parse_terminated)
709                    .map_err(|e| darling::Error::custom(e.to_string()).with_span(list))?;
710                Ok(FlagsList(parsed.into_iter().collect()))
711            }
712            _ => Err(darling::Error::custom("expected flags(Path, ...)").with_span(item)),
713        }
714    }
715}
716
717#[derive(Debug, Default, FromMeta)]
718struct RawRootAttributes {
719    prefix: Option<SpannedKv<String>>,
720    exact_prefix: Option<SpannedKv<String>>,
721
722    #[darling(default)]
723    rename_all: NameStyle,
724
725    #[darling(rename = "emf::dimension_sets")]
726    emf_dimensions: Option<DimensionSets>,
727
728    tag: Option<SpannedValue<RawTag>>,
729
730    subfield: Flag,
731    #[darling(rename = "subfield_owned")]
732    subfield_owned: Flag,
733    #[darling(rename = "sample_group")]
734    sample_group: Flag,
735    value: Option<ValueAttributes>,
736}
737
738#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
739enum MetricMode {
740    #[default]
741    RootEntry,
742    Subfield,
743    SubfieldOwned,
744    Value,
745    ValueString,
746}
747
748#[derive(Debug, Default)]
749struct RootAttributes {
750    prefix: Option<Prefix>,
751
752    rename_all: NameStyle,
753
754    emf_dimensions: Option<DimensionSets>,
755
756    tag: Option<Tag>,
757
758    sample_group: bool,
759
760    mode: MetricMode,
761}
762
763impl RawRootAttributes {
764    fn validate(self) -> darling::Result<RootAttributes> {
765        let mut out: Option<(MetricMode, &'static str)> = None;
766        if let Some(value_attrs) = self.value {
767            if value_attrs.string.is_present() {
768                out = set_exclusive(
769                    |_| MetricMode::ValueString,
770                    "value",
771                    out,
772                    &value_attrs.string,
773                )?
774            } else {
775                out = Some((MetricMode::Value, "value"));
776            }
777        }
778        out = set_exclusive(|_| MetricMode::Subfield, "subfield", out, &self.subfield)?;
779        out = set_exclusive(
780            |_| MetricMode::SubfieldOwned,
781            "subfield_owned",
782            out,
783            &self.subfield_owned,
784        )?;
785        let mut mode = out.map(|(s, _)| s).unwrap_or_default();
786        let sample_group = if self.sample_group.is_present() {
787            if let MetricMode::Value = &mut mode {
788                true
789            } else {
790                return Err(darling::Error::custom(
791                    "`sample_group` as a top-level attribute can only be used with #[metrics(value)]",
792                )
793                .with_span(&self.sample_group.span()));
794            }
795        } else {
796            false
797        };
798        if let (MetricMode::ValueString, Some(ds)) = (mode, &self.emf_dimensions) {
799            return Err(
800                darling::Error::custom("value does not make sense with dimension-sets")
801                    .with_span(&ds.span()),
802            );
803        }
804        let tag = self
805            .tag
806            .map(|tag| match &mode {
807                MetricMode::RootEntry | MetricMode::Subfield | MetricMode::SubfieldOwned => {
808                    Ok(tag.into_inner().into())
809                }
810                MetricMode::Value | MetricMode::ValueString => Err(darling::Error::custom(
811                    "value and value(string) do not support tag",
812                )
813                .with_span(&tag.span())),
814            })
815            .transpose()?;
816
817        Ok(RootAttributes {
818            prefix: Prefix::from_inflectable_and_exact(
819                &self.prefix,
820                &self.exact_prefix,
821                PrefixLevel::Root,
822            )?
823            .map(SpannedValue::into_inner),
824            rename_all: self.rename_all,
825            emf_dimensions: self.emf_dimensions,
826            tag,
827            sample_group,
828            mode,
829        })
830    }
831}
832
833impl RootAttributes {
834    fn configuration_field_names(&self) -> Vec<Ts2> {
835        if let Some(_dims) = &self.emf_dimensions {
836            vec![quote! { __config__ }]
837        } else {
838            vec![]
839        }
840    }
841
842    fn configuration_fields(&self) -> Vec<Ts2> {
843        let mut fields = vec![];
844        if let Some(_dims) = &self.emf_dimensions {
845            fields.push(quote! {
846                __config__: ::metrique::emf::SetEntryDimensions
847            })
848        }
849        fields
850    }
851
852    fn create_configuration(&self) -> Vec<Ts2> {
853        let mut fields = vec![];
854        if let Some(dims) = &self.emf_dimensions {
855            fields
856                .push(quote! { __config__: ::metrique::__plumbing_entry_dimensions!(dims: #dims) })
857        }
858        fields
859    }
860
861    fn ownership_kind(&self) -> OwnershipKind {
862        match self.mode {
863            MetricMode::RootEntry | MetricMode::SubfieldOwned => OwnershipKind::ByValue,
864            MetricMode::Subfield | MetricMode::ValueString | MetricMode::Value => {
865                OwnershipKind::ByRef
866            }
867        }
868    }
869
870    fn warnings(&self) -> Ts2 {
871        quote! {}
872    }
873}
874
875#[derive(Debug, FromField)]
876#[darling(attributes(metrics))]
877struct RawMetricsFieldAttrs {
878    flatten: Flag,
879
880    flatten_entry: Flag,
881
882    no_close: Flag,
883
884    timestamp: Flag,
885
886    sample_group: Flag,
887
888    ignore: Flag,
889
890    #[darling(default)]
891    unit: Option<SpannedKv<syn::Path>>,
892
893    #[darling(default)]
894    format: Option<SpannedKv<syn::Path>>,
895
896    #[darling(default)]
897    name: Option<SpannedKv<String>>,
898
899    #[darling(default)]
900    prefix: Option<SpannedKv<String>>,
901
902    #[darling(default)]
903    exact_prefix: Option<SpannedKv<String>>,
904
905    #[darling(default)]
906    flags: FlagsList,
907}
908
909/// Wrapper type to allow recovering both the key and value span when parsing an attribute
910#[derive(Debug)]
911pub(crate) struct SpannedKv<T> {
912    pub(crate) key_span: Span,
913    #[allow(dead_code)]
914    pub(crate) value_span: Span,
915    pub(crate) value: T,
916}
917
918impl<T: FromMeta> FromMeta for SpannedKv<T> {
919    fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
920        let value = T::from_meta(item).map_err(|e| e.with_span(item))?;
921        let (key_span, value_span) = match item {
922            syn::Meta::NameValue(nv) => (nv.path.span(), nv.value.span()),
923            _ => return Err(darling::Error::custom("expected a key value pair").with_span(item)),
924        };
925
926        Ok(SpannedKv {
927            key_span,
928            value_span,
929            value,
930        })
931    }
932}
933
934pub(crate) fn parse_metric_fields(
935    fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
936) -> Result<Vec<MetricsField>> {
937    let mut parsed_fields = vec![];
938    let mut errors = darling::Error::accumulator();
939
940    for (i, field) in fields.iter().enumerate() {
941        let i = syn::Index::from(i);
942        let (ident, name, span) = match &field.ident {
943            Some(ident) => (quote! { #ident }, Some(ident.to_string()), ident.span()),
944            None => (quote! { #i }, None, field.ty.span()),
945        };
946
947        let attrs = match errors
948            .handle(RawMetricsFieldAttrs::from_field(field).and_then(|attr| attr.validate()))
949        {
950            Some(attrs) => attrs,
951            None => {
952                continue;
953            }
954        };
955
956        parsed_fields.push(MetricsField {
957            ident,
958            name,
959            span,
960            ty: field.ty.clone(),
961            vis: field.vis.clone(),
962            external_attrs: clean_attrs(&field.attrs),
963            attrs,
964        });
965    }
966
967    errors.finish()?;
968
969    Ok(parsed_fields)
970}
971
972fn cannot_combine_error(existing: &str, new: &str, new_span: Span) -> darling::Error {
973    darling::Error::custom(format!("Cannot combine `{existing}` with `{new}`")).with_span(&new_span)
974}
975
976// Set metrics to `new`, enforcing the fact that this field is exclusive and cannot be combined
977fn set_exclusive<T>(
978    new: impl Fn(Span) -> T,
979    name: &'static str,
980    existing: Option<(T, &'static str)>,
981    flag: &Flag,
982) -> darling::Result<Option<(T, &'static str)>> {
983    match (flag.is_present(), &existing) {
984        (true, Some((_, other))) => Err(cannot_combine_error(other, name, flag.span())),
985        (true, None) => Ok(Some((new(flag.span()), name))),
986        _ => Ok(existing),
987    }
988}
989
990// retrieve the value for a field, enforcing the fact that unit/name cannot be combined with other options
991fn get_field_option<'a, T>(
992    field_name: &'static str,
993    existing: &Option<(MetricsFieldKind, &'static str)>,
994    span: &'a Option<SpannedKv<T>>,
995) -> darling::Result<Option<&'a T>> {
996    match (span, &existing) {
997        (Some(input), Some((_, other))) => {
998            Err(cannot_combine_error(other, field_name, input.key_span))
999        }
1000        (Some(v), None) => Ok(Some(&v.value)),
1001        _ => Ok(None),
1002    }
1003}
1004
1005// retrieve the value for a flag that requires a value to be a field
1006fn get_field_flag(
1007    field_name: &'static str,
1008    existing: &Option<(MetricsFieldKind, &'static str)>,
1009    flag: &Flag,
1010) -> darling::Result<Option<Span>> {
1011    match (flag.is_present(), &existing) {
1012        (true, Some((_, other))) => Err(cannot_combine_error(other, field_name, flag.span())),
1013        (true, None) => Ok(Some(flag.span())),
1014        _ => Ok(None),
1015    }
1016}
1017
1018impl RawMetricsFieldAttrs {
1019    fn validate(self) -> darling::Result<MetricsFieldAttrs> {
1020        let mut out: Option<(MetricsFieldKind, &'static str)> = None;
1021        out = set_exclusive(
1022            |span| MetricsFieldKind::Flatten { span, prefix: None },
1023            "flatten",
1024            out,
1025            &self.flatten,
1026        )?;
1027        out = set_exclusive(
1028            MetricsFieldKind::FlattenEntry,
1029            "flatten_entry",
1030            out,
1031            &self.flatten_entry,
1032        )?;
1033        out = set_exclusive(
1034            MetricsFieldKind::Timestamp,
1035            "timestamp",
1036            out,
1037            &self.timestamp,
1038        )?;
1039        out = set_exclusive(MetricsFieldKind::Ignore, "ignore", out, &self.ignore)?;
1040
1041        let name = self.name.map(validate_name).transpose()?;
1042        let name = get_field_option("name", &out, &name)?;
1043        let unit = get_field_option("unit", &out, &self.unit)?;
1044        let format = get_field_option("format", &out, &self.format)?;
1045        let sample_group = get_field_flag("sample_group", &out, &self.sample_group)?;
1046        let close = !self.no_close.is_present();
1047        if let (false, Some((MetricsFieldKind::Ignore(span), _))) = (close, &out) {
1048            return Err(cannot_combine_error("no_close", "ignore", *span));
1049        }
1050
1051        let prefix = Prefix::from_inflectable_and_exact(
1052            &self.prefix,
1053            &self.exact_prefix,
1054            PrefixLevel::Field,
1055        )?;
1056        if let Some(prefix_) = prefix {
1057            match &mut out {
1058                Some((MetricsFieldKind::Flatten { prefix, .. }, _)) => {
1059                    *prefix = Some(prefix_.into_inner());
1060                }
1061                _ => {
1062                    return Err(
1063                        darling::Error::custom("prefix can only be used with `flatten`")
1064                            .with_span(&prefix_.span()),
1065                    );
1066                }
1067            }
1068        }
1069
1070        // flags(...) on flatten/flatten_entry/timestamp/ignore is not yet supported.
1071        if !self.flags.0.is_empty()
1072            && let Some((
1073                MetricsFieldKind::Flatten { span, .. }
1074                | MetricsFieldKind::FlattenEntry(span)
1075                | MetricsFieldKind::Timestamp(span)
1076                | MetricsFieldKind::Ignore(span),
1077                _,
1078            )) = &out
1079        {
1080            return Err(darling::Error::custom(
1081                "flags(...) is not yet supported on flatten, flatten_entry, timestamp, or ignore fields.",
1082            )
1083            .with_span(span));
1084        }
1085
1086        Ok(MetricsFieldAttrs {
1087            close,
1088            kind: match out {
1089                Some((out, _)) => out,
1090                None => MetricsFieldKind::Field {
1091                    sample_group,
1092                    name: name.cloned(),
1093                    unit: unit.cloned(),
1094                    format: format.cloned(),
1095                },
1096            },
1097            flags: self.flags.0,
1098        })
1099    }
1100}
1101
1102fn validate_name(name: SpannedKv<String>) -> darling::Result<SpannedKv<String>> {
1103    match validate_name_inner(&name.value) {
1104        Ok(_) => Ok(name),
1105        Err(msg) => Err(darling::Error::custom(msg).with_span(&name.value_span)),
1106    }
1107}
1108
1109fn validate_name_inner(name: &str) -> std::result::Result<(), &'static str> {
1110    if name.is_empty() {
1111        return Err("invalid name: name field must not be empty");
1112    }
1113
1114    if name.contains(' ') {
1115        return Err("invalid name: name must not contain spaces");
1116    }
1117    Ok(())
1118}
1119
1120#[derive(Debug, Clone)]
1121struct MetricsFieldAttrs {
1122    close: bool,
1123    kind: MetricsFieldKind,
1124    flags: Vec<syn::Path>,
1125}
1126
1127pub(crate) struct MetricsField {
1128    pub(crate) vis: Visibility,
1129    pub(crate) ident: Ts2,
1130    pub(crate) name: Option<String>,
1131    pub(crate) span: Span,
1132    pub(crate) ty: Type,
1133    pub(crate) external_attrs: Vec<Attribute>,
1134    pub(crate) attrs: MetricsFieldAttrs,
1135}
1136
1137impl MetricsField {
1138    /// Extract `#[cfg(...)]` and `#[cfg_attr(...)]` attributes from this field's external attrs.
1139    /// These must be propagated to generated declarations/usages so cfg-disabled fields
1140    /// are not referenced in write/close/sample_group code.
1141    pub(crate) fn cfg_attrs(&self) -> impl Iterator<Item = &Attribute> {
1142        self.external_attrs
1143            .iter()
1144            .filter(|a| a.path().is_ident("cfg") || a.path().is_ident("cfg_attr"))
1145    }
1146}
1147
1148impl MetricsField {
1149    fn core_field(&self, is_named: bool) -> Ts2 {
1150        let MetricsField {
1151            ref external_attrs,
1152            ref ident,
1153            ref ty,
1154            ref vis,
1155            ..
1156        } = *self;
1157        let field = if is_named {
1158            quote! { #ident: #ty }
1159        } else {
1160            quote! { #ty }
1161        };
1162        quote! { #(#external_attrs)* #vis #field }
1163    }
1164
1165    fn entry_field(&self, named: bool) -> Option<Ts2> {
1166        if let MetricsFieldKind::Ignore(_span) = self.attrs.kind {
1167            return None;
1168        }
1169        let MetricsField {
1170            ident, ty, span, ..
1171        } = self;
1172        let mut base_type = if self.attrs.close {
1173            quote_spanned! { *span=> <#ty as metrique::CloseValue>::Closed }
1174        } else {
1175            quote_spanned! { *span=>#ty }
1176        };
1177        if let Some(expr) = self.unit() {
1178            base_type = quote_spanned! { expr.span()=>
1179                <#base_type as ::metrique::unit::AttachUnit>::Output<#expr>
1180            }
1181        }
1182        let inner = if named {
1183            quote! { #ident: #base_type }
1184        } else {
1185            quote! { #base_type }
1186        };
1187        let cfg_attrs = self.cfg_attrs();
1188        Some(quote_spanned! { *span=>
1189                #(#cfg_attrs)*
1190                #[deprecated(note = "these fields will become private in a future release. To introspect an entry, use `metrique::writer::test_util::test_entry`")]
1191                #[doc(hidden)]
1192                #inner
1193        })
1194    }
1195
1196    fn unit(&self) -> Option<&syn::Path> {
1197        match &self.attrs.kind {
1198            MetricsFieldKind::Field { unit, .. } => unit.as_ref(),
1199            _ => None,
1200        }
1201    }
1202
1203    pub(crate) fn close_value(&self, ownership_kind: OwnershipKind) -> Ts2 {
1204        let ident = &self.ident;
1205        let span = self.span;
1206        let field_expr = match ownership_kind {
1207            OwnershipKind::ByValue => quote_spanned! {span=> __metrique_self_expr!().#ident },
1208            OwnershipKind::ByRef => quote_spanned! {span=> &__metrique_self_expr!().#ident },
1209        };
1210        self.close_field_expr(field_expr)
1211    }
1212
1213    pub(crate) fn close_field_expr(&self, field_expr: Ts2) -> Ts2 {
1214        let ident = &self.ident;
1215        let span = self.span;
1216        let base = if self.attrs.close {
1217            quote_spanned! {span=> metrique::CloseValue::close(#field_expr) }
1218        } else {
1219            field_expr
1220        };
1221
1222        let base = if let Some(unit) = self.unit() {
1223            quote_spanned! { unit.span() =>
1224                #base.into()
1225            }
1226        } else {
1227            base
1228        };
1229
1230        let cfg_attrs = self.cfg_attrs();
1231        quote! { #(#cfg_attrs)* #ident: #base }
1232    }
1233}
1234
1235pub(crate) struct TupleData {
1236    pub(crate) ty: syn::Type,
1237    pub(crate) kind: MetricsFieldKind,
1238    pub(crate) close: bool,
1239}
1240
1241/// Generate the entry type for a field/variant, optionally closing it
1242pub(crate) fn entry_type(ty: &syn::Type, close: bool, span: proc_macro2::Span) -> Ts2 {
1243    if close {
1244        quote::quote_spanned! { span=> <#ty as metrique::CloseValue>::Closed }
1245    } else {
1246        quote::quote_spanned! { span=> #ty }
1247    }
1248}
1249
1250pub(crate) enum PrefixLevel {
1251    Root,
1252    Field,
1253}
1254
1255#[derive(Debug, Clone)]
1256pub(crate) enum Prefix {
1257    Inflectable { prefix: String },
1258    Exact(String),
1259}
1260
1261impl Prefix {
1262    /// Apply prefix to base name and inflect according to name_style
1263    pub(crate) fn apply(&self, base: &str, name_style: NameStyle) -> String {
1264        match self {
1265            Prefix::Exact(exact_prefix) => {
1266                format!("{}{}", exact_prefix, name_style.apply(base))
1267            }
1268            Prefix::Inflectable { prefix } => {
1269                let prefixed = format!("{}{}", prefix, base);
1270                name_style.apply(&prefixed)
1271            }
1272        }
1273    }
1274
1275    fn inflected_prefix_message(prefix: &str, c: char) -> String {
1276        let warning_text = if name_contains_dot(prefix) {
1277            " '.' used to be allowed in `prefix` but is now forbidden."
1278        } else {
1279            ""
1280        };
1281        let prefix_fixed: String = prefix
1282            .chars()
1283            .map(|c| if !c.is_alphanumeric() { '-' } else { c })
1284            .collect();
1285        format!(
1286            "You cannot use the character {c:?} with `prefix`. `prefix` will \"inflect\" to match the name scheme specified by `rename_all`. For example, \
1287            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:?} \
1288            in the final metric name use `exact_prefix = {prefix:?}.{warning_text}"
1289        )
1290    }
1291
1292    fn prefix_should_end_with_delimiter_message(prefix: &str) -> String {
1293        let delimiter = if prefix.contains('-') { '-' } else { '_' };
1294        let prefix_fixed = format!("{prefix}{delimiter}");
1295        format!(
1296            "The root-level prefix `{prefix:?}` must end with a delimiter. Use `prefix = {prefix_fixed:?}`, which inflects \
1297            correctly in all inflections"
1298        )
1299    }
1300
1301    fn from_inflectable_and_exact(
1302        inflectable: &Option<SpannedKv<String>>,
1303        exact: &Option<SpannedKv<String>>,
1304        level: PrefixLevel,
1305    ) -> darling::Result<Option<SpannedValue<Self>>> {
1306        match (inflectable, exact) {
1307            (Some(prefix), None) => {
1308                if let Some(c) = name_contains_uninflectables(&prefix.value) {
1309                    Err(
1310                        darling::Error::custom(Self::inflected_prefix_message(&prefix.value, c))
1311                            .with_span(&prefix.key_span),
1312                    )
1313                } else if let PrefixLevel::Root = level
1314                    && !name_ends_with_delimiter(&prefix.value)
1315                {
1316                    Err(
1317                        darling::Error::custom(Self::prefix_should_end_with_delimiter_message(
1318                            &prefix.value,
1319                        ))
1320                        .with_span(&prefix.key_span),
1321                    )
1322                } else {
1323                    Ok(Some(SpannedValue::new(
1324                        Self::Inflectable {
1325                            prefix: prefix.value.clone(),
1326                        },
1327                        prefix.key_span,
1328                    )))
1329                }
1330            }
1331            (None, Some(p)) => Ok(Some(SpannedValue::new(
1332                Prefix::Exact(p.value.clone()),
1333                p.key_span,
1334            ))),
1335            (None, None) => Ok(None),
1336            (Some(inflectable), Some(_)) => Err(cannot_combine_error(
1337                "prefix",
1338                "exact_prefix",
1339                inflectable.key_span,
1340            )),
1341        }
1342    }
1343
1344    /// Append this prefix to the namespace's prefix chain.
1345    /// The prefix will be prepended to metric names when they are written.
1346    /// Returns (extra, namespace_with_prefix).
1347    pub(crate) fn append_to(
1348        &self,
1349        ns: &proc_macro2::TokenStream,
1350        span: proc_macro2::Span,
1351    ) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
1352        match self {
1353            Prefix::Inflectable { prefix } => {
1354                crate::entry_impl::make_inflect_prefix(ns, prefix, span)
1355            }
1356            Prefix::Exact(exact_prefix) => {
1357                crate::entry_impl::make_exact_prefix(ns, exact_prefix, span)
1358            }
1359        }
1360    }
1361}
1362
1363#[derive(Debug, Clone)]
1364enum MetricsFieldKind {
1365    Ignore(Span),
1366    Flatten {
1367        span: Span,
1368        prefix: Option<Prefix>,
1369    },
1370    FlattenEntry(Span),
1371    Timestamp(Span),
1372    Field {
1373        unit: Option<syn::Path>,
1374        name: Option<String>,
1375        format: Option<syn::Path>,
1376        sample_group: Option<Span>,
1377    },
1378}
1379
1380// produce a warning that the user can see
1381//
1382// currently, we do not have any logic that produces warnings, but leave this
1383// in for the next time
1384#[allow(unused)]
1385fn proc_macro_warning(span: Span, warning: &str) -> Ts2 {
1386    quote_spanned! {span=>
1387        const _: () = {
1388            #[deprecated(note=#warning)]
1389            const _W: () = ();
1390            _W
1391        };
1392    }
1393}
1394
1395fn parse_root_attrs(attr: TokenStream) -> Result<RootAttributes> {
1396    let nested_meta = NestedMeta::parse_meta_list(attr.into())?;
1397    Ok(RawRootAttributes::from_list(&nested_meta)?.validate()?)
1398}
1399
1400fn generate_metrics(root_attributes: RootAttributes, input: DeriveInput) -> Result<Ts2> {
1401    // Check if #[aggregate] attribute is present
1402    if input
1403        .attrs
1404        .iter()
1405        .any(|attr| attr.path().is_ident("aggregate"))
1406    {
1407        return Err(Error::new_spanned(
1408            &input,
1409            "#[aggregate] must be placed before #[metrics], not after",
1410        ));
1411    }
1412
1413    let output = match root_attributes.mode {
1414        MetricMode::RootEntry | MetricMode::Subfield | MetricMode::SubfieldOwned => {
1415            match &input.data {
1416                Data::Struct(data_struct) => {
1417                    if root_attributes.tag.is_some() {
1418                        return Err(Error::new_spanned(
1419                            &input,
1420                            "`tag` attribute is only supported on entry enums",
1421                        ));
1422                    }
1423                    let fields = match &data_struct.fields {
1424                        Fields::Named(fields_named) => &fields_named.named,
1425                        _ => {
1426                            return Err(Error::new_spanned(
1427                                &input,
1428                                "Only named fields are supported",
1429                            ));
1430                        }
1431                    };
1432                    structs::generate_metrics_for_struct(root_attributes, &input, fields)?
1433                }
1434                Data::Enum(data_enum) => {
1435                    let variants =
1436                        enums::parse_enum_variants(&data_enum.variants, enums::VariantMode::Entry)?;
1437                    enums::generate_metrics_for_enum(root_attributes, &input, &variants)?
1438                }
1439                Data::Union(_) => {
1440                    return Err(Error::new_spanned(
1441                        &input,
1442                        "Only structs and enums are supported for entries",
1443                    ));
1444                }
1445            }
1446        }
1447        MetricMode::Value => match &input.data {
1448            Data::Struct(data_struct) => {
1449                let fields = match &data_struct.fields {
1450                    Fields::Named(fields_named) => &fields_named.named,
1451                    Fields::Unnamed(fields_unnamed) => &fields_unnamed.unnamed,
1452                    _ => {
1453                        return Err(Error::new_spanned(
1454                            &input,
1455                            "Only named fields are supported",
1456                        ));
1457                    }
1458                };
1459                structs::generate_metrics_for_struct(root_attributes, &input, fields)?
1460            }
1461            _ => {
1462                return Err(Error::new_spanned(
1463                    &input,
1464                    "Only structs are supported with value, use value(string) with enums",
1465                ));
1466            }
1467        },
1468        MetricMode::ValueString => {
1469            let variants = match &input.data {
1470                Data::Enum(data_enum) => &data_enum.variants,
1471                _ => {
1472                    return Err(Error::new_spanned(
1473                        &input,
1474                        "Only enums are supported with value(string)",
1475                    ));
1476                }
1477            };
1478            let variants = enums::parse_enum_variants(variants, enums::VariantMode::ValueString)?;
1479            enums::generate_metrics_for_enum(root_attributes, &input, &variants)?
1480        }
1481    };
1482
1483    if std::env::var("MACRO_DEBUG").is_ok() {
1484        eprintln!("{}", &output);
1485    }
1486
1487    Ok(output)
1488}
1489
1490/// Generates `Ident<'static, 'static, T, N, ...>` with all lifetimes replaced by 'static
1491fn with_static_lifetimes(ident: &Ident, generics: &Generics) -> Ts2 {
1492    if generics.params.is_empty() {
1493        return quote! { #ident };
1494    }
1495
1496    let args = generics.params.iter().map(|param| match param {
1497        GenericParam::Lifetime(_) => quote! { 'static },
1498        GenericParam::Type(ty) => {
1499            let ident = &ty.ident;
1500            quote! { #ident }
1501        }
1502        GenericParam::Const(c) => {
1503            let ident = &c.ident;
1504            quote! { #ident }
1505        }
1506    });
1507
1508    quote! { #ident<#(#args),*> }
1509}
1510
1511/// Generate the on_drop_wrapper implementation
1512pub(crate) fn generate_on_drop_wrapper(
1513    vis: &Visibility,
1514    guard: &Ident,
1515    inner: &Ident,
1516    target: &Ident,
1517    handle: &Ident,
1518    generics: &Generics,
1519) -> Ts2 {
1520    let inner_str = inner.to_string();
1521    let guard_str = guard.to_string();
1522
1523    let (_impl_generics, _, where_clause) = generics.split_for_impl();
1524    let inner_static = with_static_lifetimes(inner, generics);
1525    let target_static = with_static_lifetimes(target, generics);
1526
1527    quote! {
1528        #[doc = concat!("Metrics guard returned from [`", #inner_str, "::append_on_drop`], closes the entry and appends the metrics to a sink when dropped.")]
1529        #vis type #guard<Q = ::metrique::DefaultSink> = ::metrique::AppendAndCloseOnDrop<#inner_static, Q>;
1530
1531        #[doc = concat!("Metrics handle returned from [`", #guard_str, "::handle`], similar to an `Arc<", #guard_str, ">`.")]
1532        #vis type #handle<Q = ::metrique::DefaultSink> = ::metrique::AppendAndCloseOnDropHandle<#inner_static, Q>;
1533
1534        impl #inner_static #where_clause {
1535            #[doc = "Creates an AppendAndCloseOnDrop that will be automatically appended to `sink` on drop."]
1536            #vis fn append_on_drop<Q: ::metrique::writer::EntrySink<::metrique::RootEntry<#target_static>> + Send + Sync + 'static>(self, sink: Q) -> #guard<Q> {
1537                ::metrique::append_and_close(self, sink)
1538            }
1539        }
1540    }
1541}
1542
1543fn generate_close_value_impls(
1544    root_attrs: &RootAttributes,
1545    base_ty: &Ident,
1546    closed_ty: &Ident,
1547    generics: &syn::Generics,
1548    impl_body: Ts2,
1549) -> Ts2 {
1550    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1551
1552    let (metrics_struct_ty, proxy_impl) = match root_attrs.ownership_kind() {
1553        OwnershipKind::ByValue => (quote!(#base_ty #ty_generics), quote!()),
1554        OwnershipKind::ByRef => (
1555            quote!(&'_ #base_ty #ty_generics),
1556            // for a by-ref ownership, also add a proxy impl for by-value
1557            quote!(impl #impl_generics metrique::CloseValue for #base_ty #ty_generics #where_clause {
1558                type Closed = #closed_ty #ty_generics;
1559                fn close(self) -> Self::Closed {
1560                    <&Self>::close(&self)
1561                }
1562            }),
1563        ),
1564    };
1565
1566    let close_fn = quote! {
1567        fn close(self) -> Self::Closed {
1568            // `self` is expanded from macro_rules! input in some callers
1569            // Routing receiver access through a local macro preserves hygiene in those cases,
1570            // while avoiding the extra diagnostic site caused by other approaches like rebinding self.
1571            macro_rules! __metrique_self_expr {
1572                () => {
1573                    self
1574                };
1575            }
1576            #impl_body
1577        }
1578    };
1579
1580    quote! {
1581        impl #impl_generics metrique::CloseValue for #metrics_struct_ty #where_clause {
1582            type Closed = #closed_ty #ty_generics;
1583            #close_fn
1584        }
1585
1586        #proxy_impl
1587    }
1588}
1589
1590pub(crate) fn clean_attrs(attr: &[Attribute]) -> Vec<Attribute> {
1591    attr.iter()
1592        .filter(|attr| !attr.path().is_ident("metrics"))
1593        .cloned()
1594        .collect()
1595}
1596
1597/// Minimal passthrough that strips #[metrics] attributes from struct fields.
1598///
1599/// If the proc macro fails, then absent anything else, the struct provider by the user will
1600/// not exist in code. This ensures that even if the proc macro errors, the struct will still be present
1601/// making finding the actual cause of the compiler errors much easier.
1602///
1603/// This function is not used in the happy path case, but if we encounter errors in the
1604/// main pass, this is returned along with the compiler error to remove spurious compiler
1605/// failures.
1606fn clean_base_adt(input: &DeriveInput) -> Ts2 {
1607    let adt_name = &input.ident;
1608    let vis = &input.vis;
1609    let generics = &input.generics;
1610
1611    // Filter out any #[metrics] attributes from the struct
1612    let filtered_attrs = clean_attrs(&input.attrs);
1613    match &input.data {
1614        Data::Struct(data_struct) => match &data_struct.fields {
1615            Fields::Named(fields_named) => {
1616                structs::clean_base_struct(vis, adt_name, generics, filtered_attrs, fields_named)
1617            }
1618            Fields::Unnamed(fields_unnamed) => structs::clean_base_unnamed_struct(
1619                vis,
1620                adt_name,
1621                generics,
1622                filtered_attrs,
1623                fields_unnamed,
1624            ),
1625            // In these cases, we can't strip attributes since we don't support this format.
1626            // Echo back exactly what was given.
1627            _ => input.to_token_stream(),
1628        },
1629        Data::Enum(data_enum) => {
1630            if let Ok(variants) = enums::parse_enum_variants(
1631                &data_enum.variants,
1632                enums::VariantMode::SkipAttributeParsing,
1633            ) {
1634                enums::generate_base_enum(adt_name, vis, generics, &filtered_attrs, &variants)
1635            } else {
1636                input.to_token_stream()
1637            }
1638        }
1639        _ => input.to_token_stream(),
1640    }
1641}
1642
1643#[cfg(test)]
1644mod tests {
1645    use darling::FromMeta;
1646    use insta::assert_snapshot;
1647    use proc_macro2::TokenStream as Ts2;
1648    use quote::quote;
1649    use syn::{parse_quote, parse2};
1650
1651    use crate::RawRootAttributes;
1652
1653    // Helper function to convert proc_macro::TokenStream to proc_macro2::TokenStream
1654    // This allows us to test the macro without needing to use the proc_macro API directly
1655    fn metrics_impl(input: Ts2, attrs: Ts2) -> Ts2 {
1656        let input = syn::parse2(input).unwrap();
1657        let meta: syn::Meta = syn::parse2(attrs).unwrap();
1658        let root_attrs = RawRootAttributes::from_meta(&meta)
1659            .unwrap()
1660            .validate()
1661            .unwrap();
1662        super::generate_metrics(root_attrs, input).unwrap()
1663    }
1664
1665    fn metrics_impl_string(input: Ts2, attrs: Ts2) -> String {
1666        let output = metrics_impl(input, attrs);
1667
1668        // Parse the output back into a syn::File for pretty printing
1669        match parse2::<syn::File>(output.clone()) {
1670            Ok(file) => prettyplease::unparse(&file),
1671            Err(_) => {
1672                // If parsing fails, print the error and use the raw string output
1673                output.to_string()
1674            }
1675        }
1676    }
1677
1678    #[test]
1679    fn test_darling_root_attrs() {
1680        use darling::FromMeta;
1681        RawRootAttributes::from_meta(&parse_quote! {
1682            metrics(
1683                rename_all = "PascalCase",
1684                emf::dimension_sets = [["bar"]]
1685            )
1686        })
1687        .unwrap()
1688        .validate()
1689        .unwrap();
1690    }
1691
1692    #[test]
1693    fn test_simple_metrics_struct() {
1694        let input = quote! {
1695            struct RequestMetrics {
1696                operation: &'static str,
1697                number_of_ducks: usize
1698            }
1699        };
1700
1701        let parsed_file = metrics_impl_string(input, quote!(metrics()));
1702        assert_snapshot!("simple_metrics_struct", parsed_file);
1703    }
1704
1705    #[test]
1706    fn test_sample_group_metrics_struct() {
1707        let input = quote! {
1708            struct RequestMetrics {
1709                #[metrics(sample_group)]
1710                operation: &'static str,
1711                number_of_ducks: usize
1712            }
1713        };
1714
1715        let parsed_file = metrics_impl_string(input, quote!(metrics()));
1716        assert_snapshot!("sample_group_metrics_struct", parsed_file);
1717    }
1718
1719    #[test]
1720    fn test_simple_metrics_value_struct() {
1721        let input = quote! {
1722            struct RequestValue {
1723                #[metrics(ignore)]
1724                ignore: u32,
1725                value: u32,
1726            }
1727        };
1728
1729        let parsed_file = metrics_impl_string(input, quote!(metrics(value)));
1730        assert_snapshot!("simple_metrics_value_struct", parsed_file);
1731    }
1732
1733    #[test]
1734    fn test_sample_group_metrics_value_struct() {
1735        let input = quote! {
1736            struct RequestValue {
1737                #[metrics(ignore)]
1738                ignore: u32,
1739                value: &'static str,
1740            }
1741        };
1742
1743        let parsed_file = metrics_impl_string(input, quote!(metrics(value, sample_group)));
1744        assert_snapshot!("sample_group_metrics_value_struct", parsed_file);
1745    }
1746
1747    #[test]
1748    fn test_simple_metrics_value_unnamed_struct() {
1749        let input = quote! {
1750            struct RequestValue(
1751                #[metrics(ignore)]
1752                u32,
1753                u32);
1754        };
1755
1756        let parsed_file = metrics_impl_string(input, quote!(metrics(value)));
1757        assert_snapshot!("simple_metrics_value_unnamed_struct", parsed_file);
1758    }
1759
1760    #[test]
1761    fn test_simple_metrics_enum() {
1762        let input = quote! {
1763            enum Foo {
1764                Bar
1765            }
1766        };
1767
1768        let parsed_file = metrics_impl_string(input, quote!(metrics(value(string))));
1769        assert_snapshot!("simple_metrics_enum", parsed_file);
1770    }
1771
1772    #[test]
1773    fn test_exact_prefix_struct() {
1774        let input = quote! {
1775            struct RequestMetrics {
1776                operation: &'static str,
1777                number_of_ducks: usize
1778            }
1779        };
1780
1781        let parsed_file = metrics_impl_string(input, quote!(metrics(exact_prefix = "API@")));
1782        assert_snapshot!("exact_prefix_struct", parsed_file);
1783    }
1784
1785    #[test]
1786    fn test_field_exact_prefix_struct() {
1787        let input = quote! {
1788            struct RequestMetrics {
1789                #[metrics(flatten, exact_prefix = "API@")]
1790                nested: NestedMetrics,
1791                operation: &'static str
1792            }
1793        };
1794
1795        let parsed_file = metrics_impl_string(input, quote!(metrics()));
1796        assert_snapshot!("field_exact_prefix_struct", parsed_file);
1797    }
1798
1799    #[test]
1800    fn test_aggregate_after_metrics_error() {
1801        let input = quote! {
1802            #[metrics]
1803            #[aggregate]
1804            struct ApiCall {
1805                latency: Duration,
1806            }
1807        };
1808
1809        let input = syn::parse2(input).unwrap();
1810        let root_attrs = RawRootAttributes::from_meta(&parse_quote!(metrics()))
1811            .unwrap()
1812            .validate()
1813            .unwrap();
1814        let result = super::generate_metrics(root_attrs, input);
1815        assert!(result.is_err());
1816        let err = result.unwrap_err();
1817        assert!(
1818            err.to_string()
1819                .contains("#[aggregate] must be placed before #[metrics]")
1820        );
1821    }
1822
1823    #[test]
1824    fn test_metrics_with_lifetime() {
1825        let input = quote! {
1826            struct Foo<'a> {
1827                a: &'a str,
1828                b: usize
1829            }
1830        };
1831
1832        let parsed_file = metrics_impl_string(input, quote!(metrics()));
1833        assert_snapshot!("metrics_with_lifetime", parsed_file);
1834    }
1835
1836    #[test]
1837    fn test_metrics_with_cow_lifetime() {
1838        let input = quote! {
1839            struct Foo<'a> {
1840                a: Cow<'a, str>,
1841                b: usize
1842            }
1843        };
1844
1845        let parsed_file = metrics_impl_string(input, quote!(metrics()));
1846        assert_snapshot!("metrics_with_cow_lifetime", parsed_file);
1847    }
1848
1849    #[test]
1850    fn test_field_inflectable_prefix_struct() {
1851        let input = quote! {
1852            struct RequestMetrics {
1853                #[metrics(flatten, prefix = "api_")]
1854                nested: NestedMetrics,
1855                operation: &'static str
1856            }
1857        };
1858
1859        let parsed_file = metrics_impl_string(input, quote!(metrics()));
1860        assert_snapshot!("field_inflectable_prefix_struct", parsed_file);
1861    }
1862
1863    #[test]
1864    fn test_entry_enum() {
1865        let nested = metrics_impl_string(
1866            quote! {
1867                #[metrics(subfield)]
1868                struct Nested {
1869                    value: u32,
1870                }
1871            },
1872            quote!(metrics(subfield)),
1873        );
1874        let status = metrics_impl_string(
1875            quote! {
1876                #[metrics(subfield)]
1877                enum Status {
1878                    Active {
1879                        count: u32,
1880                        #[metrics(unit = metrique::writer::unit::Millisecond)]
1881                        latency: u64,
1882                    },
1883                    Pending(#[metrics(flatten)] Nested),
1884                    Multi(
1885                        #[metrics(flatten)] Nested,
1886                        #[metrics(ignore)] u32,
1887                    ),
1888                }
1889            },
1890            quote!(metrics(subfield)),
1891        );
1892        let root = metrics_impl_string(
1893            quote! {
1894                enum Operation {
1895                    Read { bytes: u64 },
1896                    Write(#[metrics(flatten)] Nested),
1897                }
1898            },
1899            quote!(metrics()),
1900        );
1901
1902        let parsed_file = format!("{}\n{}\n{}", nested, status, root);
1903        assert_snapshot!("entry_enum", parsed_file);
1904    }
1905
1906    #[test]
1907    fn test_subfield_struct() {
1908        let input = quote! {
1909            #[metrics(subfield)]
1910            struct NestedMetrics {
1911                counter: u32,
1912            }
1913        };
1914
1915        let parsed_file = metrics_impl_string(input, quote!(metrics(subfield)));
1916        assert_snapshot!("subfield_struct", parsed_file);
1917    }
1918
1919    #[test]
1920    fn test_sample_group_entry_enum() {
1921        let operation = metrics_impl_string(
1922            quote! {
1923                #[metrics(value(string))]
1924                enum Operation {
1925                    Read,
1926                    Write,
1927                }
1928            },
1929            quote!(metrics(value(string))),
1930        );
1931        let metadata = metrics_impl_string(
1932            quote! {
1933                #[metrics(subfield)]
1934                struct Metadata {
1935                    #[metrics(sample_group)]
1936                    operation: Operation,
1937                    request_id: String,
1938                }
1939            },
1940            quote!(metrics(subfield)),
1941        );
1942        let result = metrics_impl_string(
1943            quote! {
1944                enum RequestResult {
1945                    Success {
1946                        #[metrics(sample_group)]
1947                        operation: Operation,
1948                        bytes: usize,
1949                    },
1950                    Error {
1951                        #[metrics(sample_group)]
1952                        operation: Operation,
1953                        error_code: u32,
1954                    },
1955                    Timeout(#[metrics(flatten)] Metadata),
1956                    Cancelled(
1957                        #[metrics(flatten)] Metadata,
1958                        #[metrics(flatten_entry, no_close)] StatusEntry
1959                    ),
1960                }
1961            },
1962            quote!(metrics()),
1963        );
1964
1965        let parsed_file = format!("{}\n{}\n{}", operation, metadata, result);
1966        assert_snapshot!("sample_group_entry_enum", parsed_file);
1967    }
1968
1969    #[test]
1970    fn test_entry_enum_tag() {
1971        let nested = metrics_impl_string(
1972            quote! {
1973                #[metrics(subfield)]
1974                struct Nested {
1975                    value: u32,
1976                }
1977            },
1978            quote!(metrics(subfield)),
1979        );
1980        let root = metrics_impl_string(
1981            quote! {
1982                #[metrics(tag(name = "operation"))]
1983                enum Operation {
1984                    Read { bytes: usize },
1985                    Write(#[metrics(flatten)] Nested),
1986                }
1987            },
1988            quote!(metrics(tag(name = "operation"))),
1989        );
1990
1991        let parsed_file = format!("{}\n{}", nested, root);
1992        assert_snapshot!("entry_enum_tag", parsed_file);
1993    }
1994
1995    #[test]
1996    fn test_entry_enum_tag_with_sample_group() {
1997        let root = metrics_impl_string(
1998            quote! {
1999                #[metrics(tag(name = "operation", sample_group))]
2000                enum Operation {
2001                    Read { bytes: usize },
2002                }
2003            },
2004            quote!(metrics(tag(name = "operation", sample_group))),
2005        );
2006
2007        assert_snapshot!("entry_enum_tag_sample_group", root);
2008    }
2009
2010    #[test]
2011    fn test_debug_derive_passthrough_struct() {
2012        let input = quote! {
2013            #[derive(Debug, Clone)]
2014            struct Metrics {
2015                field: usize,
2016            }
2017        };
2018
2019        let parsed_file = metrics_impl_string(input, quote!(metrics()));
2020        assert_snapshot!("debug_derive_passthrough_struct", parsed_file);
2021    }
2022
2023    #[test]
2024    fn test_debug_derive_passthrough_enum() {
2025        let input = quote! {
2026            #[derive(Debug)]
2027            enum Operation {
2028                Read,
2029                Write,
2030            }
2031        };
2032
2033        let parsed_file = metrics_impl_string(input, quote!(metrics(value(string))));
2034        assert_snapshot!("debug_derive_passthrough_enum", parsed_file);
2035    }
2036}