Skip to main content

metrique_writer_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
8use std::collections::HashSet;
9
10use darling::{FromAttributes as _, util::SpannedValue};
11use proc_macro2::{Literal, Span, TokenStream};
12use quote::{ToTokens, quote, quote_spanned};
13use syn::{Attribute, Path, spanned::Spanned};
14use synstructure::{BindingInfo, Structure, VariantInfo};
15
16macro_rules! decl_derive {
17    ($name:ident, $derive_fn:ident) => {
18synstructure::decl_derive!([$name, attributes(entry)] =>
19    /// Derive `Entry` for a struct or enum.
20    ///
21    /// Each field in the struct or enum variant will be written to the metric entry using the rust field name by
22    /// default. For example,
23    /// ```ignore
24    /// #[derive(Entry)]
25    /// struct MySimpleEntry {
26    ///     first: String,
27    ///     second: u64,
28    ///     third: Option<bool>,
29    /// }
30    /// ```
31    /// will impl `Entry` like
32    /// ```ignore
33    /// impl Entry for MySimpleEntry {
34    ///     fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
35    ///         writer.value("first", &self.first);
36    ///         writer.value("second", &self.second);
37    ///         writer.value("third", &self.third);
38    ///     }
39    /// }
40    /// ```
41    ///
42    /// # Container attributes
43    ///
44    /// The `#[entry]` attribute can be attached to structs or enums to customize the following:
45    ///  * `#[entry(rename_all = {case})]` to rename all fields in the given `case` pattern. This is helpful when the
46    ///    expected metric name pattern doesn't match Rust's default `snake_case`. `case` can be one of
47    ///    * `"lowercase"`
48    ///    * `"UPPERCASE"`
49    ///    * `"PascalCase"`
50    ///    * `"camelCase"`
51    ///    * `"snake_case"`
52    ///    * `"SCREAMING_SNAKE_CASE"`
53    ///    * `"kebab-case"`
54    ///    * `"SCREAMING-KEBAB-CASE"`
55    ///
56    /// # Field attributes
57    ///
58    /// The `#[entry]` attribute can be attached to fields of structs, tuples, and enums to customize the following:
59    ///  * `#[entry(name = "{name}")]` to override the default name (including any case changes from `rename_all`)
60    ///  * `#[entry(ignore)]` to not write the field to the metrics entry
61    ///  * `#[entry(flatten)]` to treat the field as a sub-entry whose contents will be merged with the current entry.
62    ///    Note that any `sample_group` will be concatenated to this entry's!
63    ///  * `#[entry(timestamp)]` to treat the field as the entry's timestamp. Note that it must impl
64    ///    `Into<SystemTime>`!
65    ///  * `#[entry(sample_group)]` to treat the field as part of the entry's `sample_group`. The field's name (
66    ///     optionally overwritten by the `name` attribute) will be used as the key. Note that the field value must be
67    ///     cloneable and impl `Into<Cow<'static, str>>`!
68    ///  * `#[entry(format = FORMATTER)]` to format the field using a custom format, which should be a type
69    ///    implementing `ValueFormatter`.
70    ///
71    /// # Enums
72    ///
73    /// Each enum variant is treated as if it was a separate metric entry. This is useful when multiple, distinct
74    /// operations can be output to the same sink! For example,
75    /// ```ignore
76    /// #[derive(Entry)]
77    /// enum Operation {
78    ///     Simple {
79    ///         count: u64,
80    ///     }
81    ///     Read(#[entry(flatten)] ReadEntry),
82    ///     Write(#[entry(flatten)] WriteEntry),
83    /// }
84    ///
85    /// #[derive(Entry)]
86    /// struct ReadEntry; // ...
87    ///
88    /// #[derive(Entry)]
89    /// struct WriteEntry; // ...
90    /// ```
91    /// will impl `Entry` like
92    /// ```ignore
93    /// impl Entry for Operation {
94    ///     fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
95    ///         match self {
96    ///             Self::Simple { count } => {
97    ///                 writer.value("count", count);
98    ///             }
99    ///             Self::Read(read) => {
100    ///                 read.write(writer);
101    ///             }
102    ///             Self::Write(write) => {
103    ///                 write.write(writer);
104    ///             }
105    ///         }
106    ///     }
107    /// }
108    /// ```
109    ///
110    /// # "Real" example
111    ///
112    /// A real AWS service outputting EMF metrics might have a set of metric structs like
113    /// ```ignore
114    /// // Common constants across all entries, to be used with EntryIoStream::merge_globals()
115    /// // Operation-specific metrics
116    /// #[derive(Entry)]
117    /// enum OperationMetrics {
118    ///     Foo(#[entry(flatten)] FooMetrics),
119    ///     Bar(#[entry(flatten)] BarMetrics),
120    /// }
121    ///
122    /// #[derive(Entry)]
123    /// #[entry(rename_all = "PascalCase")]
124    /// struct FooMetrics {
125    ///     #[entry(sample_group)]
126    ///     operation: &'static str,
127    ///     success: bool,
128    ///     retries: u32,
129    ///     remote_call: Option<Duration>,
130    /// }
131    /// ```
132    $derive_fn
133);
134    }
135}
136
137decl_derive!(Entry, derive_entry);
138decl_derive!(MetriqueEntry, derive_metrique_entry);
139
140fn derive_entry(input: Structure<'_>) -> TokenStream {
141    tokens_or_compiler_err(try_derive(input, &quote!(::metrique_writer)))
142}
143
144fn derive_metrique_entry(input: Structure<'_>) -> TokenStream {
145    tokens_or_compiler_err(try_derive(input, &quote!(::metrique::writer)))
146}
147
148// Raw per-field attributes for #[entry]
149#[derive(darling::FromAttributes)]
150#[darling(attributes(entry))]
151struct ParsedFieldMetricAttr {
152    name: Option<SpannedValue<String>>,
153    sample_group: Option<SpannedValue<()>>,
154    ignore: Option<SpannedValue<()>>,
155    flatten: Option<SpannedValue<()>>,
156    timestamp: Option<SpannedValue<()>>,
157    format: Option<SpannedValue<Path>>,
158}
159
160// Validated per-field attributes
161enum FieldMetricAttr {
162    Ignore,
163    Flatten,
164    Timestamp(Span),
165    NamedValue {
166        name: Option<SpannedValue<String>>,
167        format: Option<SpannedValue<Path>>,
168        sample_group: Option<Span>,
169    },
170}
171
172impl FieldMetricAttr {
173    fn try_parse(field_span: Span, attrs: &[Attribute]) -> syn::Result<Self> {
174        match ParsedFieldMetricAttr::from_attributes(attrs)? {
175            ParsedFieldMetricAttr {
176                name,
177                sample_group,
178                format,
179                ignore: None,
180                flatten: None,
181                timestamp: None,
182            } => {
183                if let Some(name) = name.as_ref()
184                    && name.is_empty()
185                {
186                    return Err(syn::Error::new(name.span(), "`name` can't be empty"));
187                }
188                Ok(Self::NamedValue {
189                    name,
190                    sample_group: sample_group.map(|g| g.span()),
191                    format,
192                })
193            }
194
195            ParsedFieldMetricAttr {
196                name: None,
197                sample_group: None,
198                ignore: Some(_ignore),
199                flatten: None,
200                timestamp: None,
201                format: None,
202            } => Ok(Self::Ignore),
203
204            ParsedFieldMetricAttr {
205                name: None,
206                sample_group: None,
207                ignore: None,
208                flatten: Some(_flatten),
209                timestamp: None,
210                format: None,
211            } => Ok(Self::Flatten),
212
213            ParsedFieldMetricAttr {
214                name: None,
215                sample_group: None,
216                ignore: None,
217                flatten: None,
218                timestamp: Some(timestamp),
219                format: None,
220            } => Ok(Self::Timestamp(timestamp.span())),
221
222            _ => Err(syn::Error::new(
223                field_span,
224                "can only combine `name` and `sample_group` in `#[entry]`",
225            )),
226        }
227    }
228}
229
230// Container-level attributes for #[entry]
231#[derive(darling::FromAttributes)]
232#[darling(attributes(entry))]
233struct ContainerMetricAttr {
234    rename_all: Option<SpannedValue<String>>,
235}
236
237impl ContainerMetricAttr {
238    fn merge_with_defaults_from(self, root: &Self) -> Self {
239        Self {
240            rename_all: self.rename_all.or_else(|| root.rename_all.clone()),
241        }
242    }
243}
244
245fn try_derive(input: Structure<'_>, krate: &TokenStream) -> syn::Result<TokenStream> {
246    let span = input.ast().span();
247
248    let container_attr = match &input.ast().data {
249        syn::Data::Struct(_) | syn::Data::Enum(_) => {
250            ContainerMetricAttr::from_attributes(&input.ast().attrs)?
251        }
252        syn::Data::Union(_) => {
253            return Err(syn::Error::new(span, "can't derive `Entry` for unions"));
254        }
255    };
256
257    let mut writes = Vec::new();
258    let mut sample_groups = Vec::new();
259    let has_multiple_variants = input.variants().len() > 1;
260    for variant in input.variants() {
261        let EntryVariant {
262            write,
263            sample_group,
264        } = derive_variant(variant, &container_attr, has_multiple_variants, krate)?;
265        writes.push(write);
266        sample_groups.push(sample_group);
267    }
268
269    Ok(input.gen_impl(quote_spanned! {span=>
270        gen impl #krate::core::entry::Entry for @Self {
271            fn write<'a>(&'a self, writer: &mut impl #krate::core::entry::EntryWriter<'a>) {
272                match *self {
273                    #(#writes)*
274                }
275            }
276
277            fn sample_group(&self) -> impl ::std::iter::Iterator<Item = (::std::borrow::Cow<'static, str>, ::std::borrow::Cow<'static, str>)> {
278                match *self {
279                    #(#sample_groups)*
280                }
281            }
282        }
283    }))
284}
285
286fn tokens_or_compiler_err(result: syn::Result<TokenStream>) -> TokenStream {
287    match result {
288        Ok(t) => t,
289        Err(e) => e.into_compile_error(),
290    }
291}
292
293/// Return an iterator that chains the iterators in `iterators`.
294///
295/// This calls `chain` in a binary tree fashion to avoid problems with the recursion limit,
296/// e.g. `I1.chain(I2).chain(I3.chain(I4))`
297/// Chains iterators into a balanced binary tree of `.chain()` calls.
298/// Returns `::std::iter::empty()` for empty input.
299fn make_binary_tree_chain(iterators: Vec<TokenStream>) -> TokenStream {
300    fn chain_once(stack: &mut Vec<(TokenStream, usize)>, allow_different_degree: bool) -> bool {
301        if stack.len() < 2 {
302            return false; // can't merge a stack of length < 2
303        }
304        if !allow_different_degree && (stack[stack.len() - 2].1 != stack[stack.len() - 1].1) {
305            return false; // not merging elements of different degree if not wanted
306        }
307        let (rhs, rhs_deg) = stack.pop().unwrap();
308        let (lhs, lhs_deg) = stack.pop().unwrap();
309        stack.push((
310            quote!(#lhs.chain(#rhs)),
311            std::cmp::max(lhs_deg, rhs_deg) + 1,
312        ));
313        true
314    }
315
316    let mut stack = vec![];
317    for elem in iterators {
318        stack.push((elem, 0));
319        while chain_once(&mut stack, false) {}
320    }
321    while chain_once(&mut stack, true) {}
322    if let Some((elem, _deg)) = stack.pop() {
323        elem
324    } else {
325        quote!(::std::iter::empty())
326    }
327}
328
329fn derive_variant(
330    variant: &VariantInfo,
331    root_container_attr: &ContainerMetricAttr,
332    has_multiple_variants: bool,
333    krate: &TokenStream,
334) -> syn::Result<EntryVariant> {
335    let container_attr = ContainerMetricAttr::from_attributes(variant.ast().attrs)?
336        .merge_with_defaults_from(root_container_attr);
337
338    let mut fields = FieldSet {
339        namer: Namer {
340            rename_all: container_attr
341                .rename_all
342                .map_or(Ok(None), |r| NameStyle::try_parse(r.span(), &r).map(Some))?,
343            ..Default::default()
344        },
345        ..Default::default()
346    };
347
348    let mut errors: Vec<syn::Error> = variant
349        .bindings()
350        .iter()
351        .flat_map(|field| fields.add(field, krate).err())
352        .collect();
353    if let Some(mut error) = errors.pop() {
354        // flatten any errors past the first into the first error
355        error.extend(errors);
356        Err(error)
357    } else {
358        let pat = variant.pat();
359        let FieldSet {
360            writes,
361            sample_groups,
362            ..
363        } = fields;
364
365        let write = quote!(#pat => { #(#writes)* });
366        let sample_group_iter = make_binary_tree_chain(sample_groups);
367        let sample_group = if has_multiple_variants {
368            // Without boxing, each variant will have a different iterator type and therefore wouldn't compile. Boxing
369            // coerces all of them into Box<dyn Iterator>. In the future, we could optimize this by introducing an
370            // iterator enum with one variant per iterator type (like itertool's Either).
371            quote!(#pat => Box::new(#sample_group_iter) as Box<dyn ::std::iter::Iterator<Item = _>>,)
372        } else {
373            quote!(#pat => #sample_group_iter,)
374        };
375        Ok(EntryVariant {
376            write,
377            sample_group,
378        })
379    }
380}
381
382struct EntryVariant {
383    write: TokenStream,
384    sample_group: TokenStream,
385}
386
387#[derive(Default)]
388struct FieldSet {
389    namer: Namer,
390    has_timestamp: bool,
391    writes: Vec<TokenStream>,
392    sample_groups: Vec<TokenStream>,
393}
394
395impl FieldSet {
396    fn add(&mut self, field: &BindingInfo<'_>, krate: &TokenStream) -> syn::Result<()> {
397        match FieldMetricAttr::try_parse(field.span(), &field.ast().attrs)? {
398            FieldMetricAttr::NamedValue {
399                name,
400                sample_group,
401                format,
402            } => {
403                let name = Literal::string(&if let Some(name) = name {
404                    self.namer.specified(&name)?
405                } else {
406                    self.namer.unspecified(field)?
407                });
408
409                let field_tokens: TokenStream = match format {
410                    None => field.to_token_stream(),
411                    Some(format) => {
412                        let format = &*format;
413                        quote_spanned! {field.binding.span() =>
414                            &#krate::core::value::FormattedValue::<_, #format, _>::new(#field)
415                        }
416                    }
417                };
418                self.writes.push(quote_spanned! {field.binding.span()=>
419                    #krate::core::entry::EntryWriter::value(writer, #name, #field_tokens);
420                });
421                if sample_group.is_some() {
422                    self.sample_groups
423                        .push(quote_spanned! {field.binding.span()=>
424                            ::std::iter::once((
425                                ::std::borrow::Cow::Borrowed(#name),
426                                #[allow(clippy::useless_conversion)]
427                                {
428                                    #krate::core::SampleGroup::as_sample_group(#field)
429                                },
430                            ))
431                        });
432                }
433            }
434            FieldMetricAttr::Ignore => {}
435            FieldMetricAttr::Flatten => {
436                self.writes.push(quote_spanned! {field.binding.span()=>
437                    #krate::core::entry::Entry::write(#field, writer);
438                });
439                self.sample_groups
440                    .push(quote_spanned! {field.binding.span()=>
441                        #krate::core::entry::Entry::sample_group(#field)
442                    });
443            }
444            FieldMetricAttr::Timestamp(span) => {
445                if self.has_timestamp {
446                    return Err(syn::Error::new(
447                        span,
448                        "can't have more than one `timestamp`",
449                    ));
450                } else {
451                    self.has_timestamp = true;
452                    // Note we have an explicit clippy allow so that if the timestamp is already a SystemTime, it
453                    // doesn't generate code with a warning!
454                    self.writes.push(quote_spanned! {field.binding.span()=>
455                        #[allow(clippy::useless_conversion)]
456                        {
457                            #krate::core::entry::EntryWriter::timestamp(writer, (*#field).into());
458                        }
459                    });
460                }
461            }
462        };
463        Ok(())
464    }
465}
466
467// Keeps track of what field names we've already seen to detect duplicates, plus any case renaming settings
468#[derive(Debug, Clone, PartialEq, Eq, Default)]
469struct Namer {
470    names: HashSet<String>,
471    rename_all: Option<NameStyle>,
472}
473
474impl Namer {
475    fn specified(&mut self, name: &SpannedValue<String>) -> syn::Result<String> {
476        self.try_add(name.span(), name)
477    }
478
479    fn unspecified(&mut self, field: &BindingInfo<'_>) -> syn::Result<String> {
480        let Some(ident) = field.ast().ident.as_ref() else {
481            return Err(syn::Error::new(
482                field.span(),
483                "must specify `name` for tuple fields",
484            ));
485        };
486        let name = ident.to_string();
487        let name = self.rename_all.map(|r| r.apply(&name)).unwrap_or(name);
488        self.try_add(ident.span(), &name)
489    }
490
491    fn try_add(&mut self, span: Span, name: &str) -> syn::Result<String> {
492        if self.names.insert(name.into()) {
493            Ok(name.into())
494        } else {
495            Err(syn::Error::new(
496                span,
497                format!("name `{name}` is used more than once"),
498            ))
499        }
500    }
501}
502
503#[allow(clippy::enum_variant_names)] // "Case" is part of the name...
504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
505enum NameStyle {
506    LowerCase,
507    UpperCase,
508    PascalCase,
509    CamelCase,
510    SnakeCase,
511    ScreamingSnakeCase,
512    KebabCase,
513    ScreamingKebabCase,
514}
515
516impl NameStyle {
517    fn try_parse(span: Span, style: &str) -> syn::Result<Self> {
518        match style {
519            "lowercase" => Ok(Self::LowerCase),
520            "UPPERCASE" => Ok(Self::UpperCase),
521            "PascalCase" => Ok(Self::PascalCase),
522            "camelCase" => Ok(Self::CamelCase),
523            "snake_case" => Ok(Self::SnakeCase),
524            "SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase),
525            "kebab-case" => Ok(Self::KebabCase),
526            "SCREAMING-KEBAB-CASE" => Ok(Self::ScreamingKebabCase),
527            _ => Err(syn::Error::new(
528                span,
529                format!("unknown name style `{style}`"),
530            )),
531        }
532    }
533
534    fn apply(self, name: &str) -> String {
535        use inflector::Inflector;
536        match self {
537            NameStyle::LowerCase => name.to_ascii_lowercase(),
538            NameStyle::UpperCase => name.to_ascii_uppercase(),
539            NameStyle::PascalCase => name.to_pascal_case(),
540            NameStyle::CamelCase => name.to_camel_case(),
541            NameStyle::SnakeCase => name.to_snake_case(),
542            NameStyle::ScreamingSnakeCase => name.to_screaming_snake_case(),
543            NameStyle::KebabCase => name.to_kebab_case(),
544            NameStyle::ScreamingKebabCase => name.to_kebab_case().to_ascii_uppercase(),
545        }
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    #[test]
554    fn test_binary_tree_chain() {
555        assert_eq!(
556            make_binary_tree_chain(vec![]).to_string(),
557            quote! {::std::iter::empty()}.to_string()
558        );
559        assert_eq!(
560            make_binary_tree_chain(vec![quote! {1}]).to_string(),
561            quote! {1}.to_string()
562        );
563        assert_eq!(
564            make_binary_tree_chain(vec![quote! {1}, quote! {2}]).to_string(),
565            quote! {1 .chain(2)}.to_string()
566        );
567        assert_eq!(make_binary_tree_chain(vec![quote!{1},quote!{2},quote!{3},quote!{4},quote!{5},quote!{6},quote!{7},quote!{8},quote!{9}]).to_string(),
568            quote!{1 . chain (2) . chain (3 . chain (4)) . chain (5 . chain (6) . chain (7 . chain (8))) . chain (9)}.to_string());
569        assert_eq!(make_binary_tree_chain(vec![quote!{1},quote!{2},quote!{3},quote!{4},quote!{5},quote!{6},quote!{7},quote!{8},quote!{9},quote!{10},quote!{11}]).to_string(),
570            quote!{1 . chain (2) . chain (3 . chain (4)) . chain (5 . chain (6) . chain (7 . chain (8))) . chain (9 . chain (10) . chain (11))}.to_string());
571    }
572
573    #[test]
574    fn derives_struct_entry() {
575        synstructure::test_derive! {
576            derive_entry {
577                #[entry(rename_all = "PascalCase")]
578                struct TestEntry {
579                    #[entry(timestamp)]
580                    start: SystemTime,
581                    foo: String,
582                    bar: String,
583                    #[entry(sample_group)]
584                    operation: &'static str,
585                    #[entry(name = "GREAT_COUNTER")]
586                    some_counter: u64,
587                    #[entry(ignore)]
588                    ignored: bool,
589                    #[entry(flatten)]
590                    sub_entry: SubEntry,
591                    #[entry(format = my::Formatter)]
592                    custom_format: bool,
593                }
594            }
595            expands to {
596                const _: () = {
597                    impl ::metrique_writer::core::entry::Entry for TestEntry {
598                        fn write<'a>(&'a self, writer: &mut impl ::metrique_writer::core::entry::EntryWriter<'a>) {
599                            match *self {
600                                TestEntry {
601                                    start: ref __binding_0,
602                                    foo: ref __binding_1,
603                                    bar: ref __binding_2,
604                                    operation: ref __binding_3,
605                                    some_counter: ref __binding_4,
606                                    ignored: ref __binding_5,
607                                    sub_entry: ref __binding_6,
608                                    custom_format: ref __binding_7,
609                                } => {
610                                    #[allow(clippy::useless_conversion)]
611                                    {
612                                        ::metrique_writer::core::entry::EntryWriter::timestamp(writer, (*__binding_0).into());
613                                    }
614                                    ::metrique_writer::core::entry::EntryWriter::value(writer, "Foo", __binding_1);
615                                    ::metrique_writer::core::entry::EntryWriter::value(writer, "Bar", __binding_2);
616                                    ::metrique_writer::core::entry::EntryWriter::value(writer, "Operation", __binding_3);
617                                    ::metrique_writer::core::entry::EntryWriter::value(writer, "GREAT_COUNTER", __binding_4);
618                                    ::metrique_writer::core::entry::Entry::write(__binding_6, writer);
619                                    ::metrique_writer::core::entry::EntryWriter::value(writer, "CustomFormat",
620                                        &::metrique_writer::core::value::FormattedValue::<_, my::Formatter, _>::new(__binding_7));
621                                }
622                            }
623                        }
624
625                        fn sample_group(&self) -> impl ::std::iter::Iterator<Item = (::std::borrow::Cow<'static, str>, ::std::borrow::Cow<'static, str>)> {
626                            match *self {
627                                TestEntry {
628                                    start: ref __binding_0,
629                                    foo: ref __binding_1,
630                                    bar: ref __binding_2,
631                                    operation: ref __binding_3,
632                                    some_counter: ref __binding_4,
633                                    ignored: ref __binding_5,
634                                    sub_entry: ref __binding_6,
635                                    custom_format: ref __binding_7,
636                                } =>
637                                    ::std::iter::once((
638                                            ::std::borrow::Cow::Borrowed("Operation"),
639                                            #[allow(clippy::useless_conversion)]
640                                            {
641                                                ::metrique_writer::core::SampleGroup::as_sample_group(__binding_3)
642                                            },
643                                    ))
644                                    .chain(::metrique_writer::core::entry::Entry::sample_group(__binding_6)),
645                            }
646                        }
647                    }
648                };
649            }
650            no_build
651        }
652    }
653
654    #[test]
655    fn derives_struct_entry_metrique() {
656        synstructure::test_derive! {
657            derive_metrique_entry {
658                #[entry(rename_all = "PascalCase")]
659                struct TestEntry {
660                    #[entry(timestamp)]
661                    start: SystemTime,
662                }
663            }
664            expands to {
665                const _: () = {
666                    impl ::metrique::writer::core::entry::Entry for TestEntry {
667                        fn write<'a>(&'a self, writer: &mut impl ::metrique::writer::core::entry::EntryWriter<'a>) {
668                            match *self {
669                                TestEntry {
670                                    start: ref __binding_0,
671                                } => {
672                                    #[allow(clippy::useless_conversion)]
673                                    {
674                                        ::metrique::writer::core::entry::EntryWriter::timestamp(writer, (*__binding_0).into());
675                                    }
676                                }
677                            }
678                        }
679
680                        fn sample_group(&self) -> impl ::std::iter::Iterator<Item = (::std::borrow::Cow<'static, str>, ::std::borrow::Cow<'static, str>)> {
681                            match *self {
682                                TestEntry {
683                                    start: ref __binding_0,
684                                } => ::std::iter::empty(),
685                            }
686                        }
687                    }
688                };
689            }
690            no_build
691        }
692    }
693
694    #[test]
695    fn checks_duplicate_names() {
696        synstructure::test_derive! {
697            derive_entry {
698                struct TestEntry {
699                    first: String,
700                    #[entry(name = "first")]
701                    second: String
702                }
703            }
704            expands to {
705                ::core::compile_error! { "name `first` is used more than once" }
706            }
707            no_build
708        }
709    }
710
711    #[test]
712    fn checks_duplicate_timestamps() {
713        synstructure::test_derive! {
714            derive_entry {
715                struct TestEntry {
716                    #[entry(timestamp)]
717                    first: String,
718                    #[entry(timestamp)]
719                    second: String
720                }
721            }
722            expands to {
723                ::core::compile_error! { "can't have more than one `timestamp`" }
724            }
725            no_build
726        }
727    }
728
729    #[test]
730    fn derives_enum_entry() {
731        synstructure::test_derive! {
732            derive_entry {
733                #[entry(rename_all = "PascalCase")]
734                enum TestEntry {
735                    First(#[entry(flatten)] FirstEntry),
736                    Second {
737                        #[entry(sample_group)]
738                        test: &'static str,
739                        #[entry(timestamp)]
740                        time: SystemTime,
741                        some_counter: u64,
742                    },
743                    Third(#[entry(name = "CanNameTuples")] u64)
744                }
745            }
746            expands to {
747                const _: () = {
748                    impl ::metrique_writer::core::entry::Entry for TestEntry {
749                        fn write<'a>(&'a self, writer: &mut impl ::metrique_writer::core::entry::EntryWriter<'a>) {
750                            match *self {
751                                TestEntry::First(ref __binding_0,) => {
752                                    ::metrique_writer::core::entry::Entry::write(__binding_0, writer);
753                                }
754                                TestEntry::Second { test: ref __binding_0, time: ref __binding_1, some_counter: ref __binding_2, } => {
755                                    ::metrique_writer::core::entry::EntryWriter::value(writer, "Test", __binding_0);
756                                    #[allow(clippy::useless_conversion)]
757                                    {
758                                        ::metrique_writer::core::entry::EntryWriter::timestamp(writer, (*__binding_1).into());
759                                    }
760                                    ::metrique_writer::core::entry::EntryWriter::value(writer, "SomeCounter", __binding_2);
761                                }
762                                TestEntry::Third(ref __binding_0,) => {
763                                    ::metrique_writer::core::entry::EntryWriter::value(writer, "CanNameTuples", __binding_0);
764                                }
765                            }
766                        }
767
768                        fn sample_group(&self) -> impl ::std::iter::Iterator<Item = (::std::borrow::Cow<'static, str>, ::std::borrow::Cow<'static, str>)> {
769                            match *self {
770                                TestEntry::First(ref __binding_0,) =>
771                                    Box::new(
772                                        ::metrique_writer::core::entry::Entry::sample_group(__binding_0)
773                                    ) as Box<dyn ::std::iter::Iterator<Item = _>>,
774                                TestEntry::Second { test: ref __binding_0, time: ref __binding_1, some_counter: ref __binding_2, } =>
775                                    Box::new(
776                                        ::std::iter::once((
777                                            ::std::borrow::Cow::Borrowed("Test"),
778                                            #[allow(clippy::useless_conversion)]
779                                            {
780                                                ::metrique_writer::core::SampleGroup::as_sample_group(__binding_0)
781                                            },
782                                        ))
783                                    ) as Box<dyn ::std::iter::Iterator<Item = _>>,
784                                TestEntry::Third(ref __binding_0,) =>
785                                    Box::new(
786                                        ::std::iter::empty()
787                                    ) as Box<dyn ::std::iter::Iterator<Item = _>>,
788                            }
789                        }
790                    }
791                };
792            }
793            no_build
794        }
795    }
796}