Skip to main content

metrique_writer_core/
descriptor.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Entry descriptors: compile-time structural metadata for macro-derived entries.
5//!
6//! Sinks interact with [`DescriptorRef`], which provides resolved field names,
7//! flags, shapes, and units. The underlying storage types ([`EntryDescriptor`],
8//! [`FieldDescriptor`]) are public for macro construction but sinks should use
9//! [`DescriptorRef`] and [`FieldView`] accessors.
10//!
11//! See the ["Recipe: a descriptor-aware sink"](https://docs.rs/metrique/latest/metrique/_guide/extending/index.html#recipe-a-descriptor-aware-sink)
12//! section in the extending guide for usage patterns and best practices.
13
14use std::any::TypeId;
15
16use smallvec::SmallVec;
17
18use crate::Unit;
19
20/// A descriptor name style definition.
21#[derive(Debug, Clone, Copy)]
22#[non_exhaustive]
23pub struct Style {
24    /// Index into per-style name arrays.
25    pub index: u8,
26    /// Human-readable name for this style (used in codegen identifiers).
27    pub name: &'static str,
28}
29
30/// All supported descriptor name styles. Single source of truth for style ordering.
31///
32/// Adding a new style means adding an entry here and a corresponding method on
33/// [`FieldDescriptorBuilder`].
34#[non_exhaustive]
35pub struct Styles;
36
37impl Styles {
38    /// No transformation (field name used as declared).
39    pub const PRESERVE: Style = Style {
40        index: 0,
41        name: "preserve",
42    };
43    /// PascalCase.
44    pub const PASCAL: Style = Style {
45        index: 1,
46        name: "PascalCase",
47    };
48    /// snake_case.
49    pub const SNAKE: Style = Style {
50        index: 2,
51        name: "snake_case",
52    };
53    /// kebab-case.
54    pub const KEBAB: Style = Style {
55        index: 3,
56        name: "kebab-case",
57    };
58    /// SCREAMING_SNAKE_CASE.
59    pub const SCREAMING_SNAKE: Style = Style {
60        index: 4,
61        name: "SCREAMING_SNAKE_CASE",
62    };
63    /// All styles in index order.
64    pub const ALL: &'static [Style] = &[
65        Self::PRESERVE,
66        Self::PASCAL,
67        Self::SNAKE,
68        Self::KEBAB,
69        Self::SCREAMING_SNAKE,
70    ];
71    /// Number of styles.
72    pub const COUNT: usize = Self::ALL.len();
73}
74
75/// Static descriptor storage for a macro-derived entry.
76#[derive(Debug)]
77pub struct EntryDescriptor {
78    name: &'static str,
79    fields: &'static [FieldDescriptor],
80    timestamp: Option<TimestampDescriptor>,
81}
82
83impl EntryDescriptor {
84    /// Create a builder for an [`EntryDescriptor`] with the given name and fields.
85    pub const fn builder(
86        name: &'static str,
87        fields: &'static [FieldDescriptor],
88    ) -> EntryDescriptorBuilder {
89        EntryDescriptorBuilder {
90            name,
91            fields,
92            timestamp: None,
93        }
94    }
95}
96
97/// Static field storage. Stores resolved names for all name styles.
98#[derive(Debug)]
99pub struct FieldDescriptor {
100    names: [&'static str; Styles::COUNT],
101    flags: &'static [FieldFlag],
102    skipped_flags: &'static [FieldFlag],
103    shape: FieldShape<'static>,
104    unit: Option<Unit>,
105}
106
107impl FieldDescriptor {
108    /// Create a builder for a [`FieldDescriptor`] with a fixed name used for all styles.
109    ///
110    /// The name is used verbatim regardless of the parent's `rename_all` style.
111    /// Call `.pascal()`, `.snake()`, `.kebab()` on the builder to set
112    /// style-specific names if needed.
113    pub const fn builder(name: &'static str) -> FieldDescriptorBuilder {
114        FieldDescriptorBuilder {
115            names: [name; Styles::COUNT],
116            flags: &[],
117            skipped_flags: &[],
118            shape: FieldShape::Opaque,
119            unit: None,
120        }
121    }
122}
123
124/// Describes the timestamp field of an entry.
125#[derive(Debug)]
126pub struct TimestampDescriptor {
127    name: &'static str,
128}
129
130impl TimestampDescriptor {
131    /// Create a [`TimestampDescriptor`] with the given name.
132    pub const fn new(name: &'static str) -> Self {
133        Self { name }
134    }
135
136    /// Field name as emitted through `EntryWriter::timestamp`.
137    pub fn name(&self) -> &str {
138        self.name
139    }
140}
141
142/// Result of calling `Entry::descriptors()`.
143#[derive(Debug, Clone)]
144#[non_exhaustive]
145pub enum Descriptors<'a> {
146    /// Descriptors are available for this entry.
147    Available(AvailableDescriptors<'a>),
148    /// This entry has not implemented descriptor support.
149    Unavailable,
150}
151
152/// Opaque container of available descriptor segments.
153#[derive(Debug, Clone)]
154pub struct AvailableDescriptors<'a>(SmallVec<[DescriptorRef<'a>; 2]>);
155
156impl<'a> AvailableDescriptors<'a> {
157    /// Iterate over the descriptor segments in write order.
158    pub fn iter(&self) -> impl Iterator<Item = &DescriptorRef<'a>> {
159        self.0.iter()
160    }
161
162    /// Number of descriptor segments.
163    pub fn len(&self) -> usize {
164        self.0.len()
165    }
166
167    /// Whether there are no descriptor segments.
168    pub fn is_empty(&self) -> bool {
169        self.0.is_empty()
170    }
171}
172
173impl<'a> std::ops::Index<usize> for AvailableDescriptors<'a> {
174    type Output = DescriptorRef<'a>;
175    fn index(&self, index: usize) -> &Self::Output {
176        &self.0[index]
177    }
178}
179
180impl<'a> IntoIterator for AvailableDescriptors<'a> {
181    type Item = DescriptorRef<'a>;
182    type IntoIter = DescriptorIter<'a>;
183    fn into_iter(self) -> Self::IntoIter {
184        DescriptorIter(self.0.into_iter())
185    }
186}
187
188/// Owned iterator over descriptor segments. Returned by `AvailableDescriptors::into_iter()`.
189#[derive(Debug)]
190pub struct DescriptorIter<'a>(smallvec::IntoIter<[DescriptorRef<'a>; 2]>);
191
192impl<'a> Iterator for DescriptorIter<'a> {
193    type Item = DescriptorRef<'a>;
194    fn next(&mut self) -> Option<Self::Item> {
195        self.0.next()
196    }
197
198    fn size_hint(&self) -> (usize, Option<usize>) {
199        self.0.size_hint()
200    }
201}
202
203impl<'a> ExactSizeIterator for DescriptorIter<'a> {}
204
205impl<'a> Descriptors<'a> {
206    /// Create an `Available` result from an iterator of descriptors.
207    pub fn available(iter: impl IntoIterator<Item = DescriptorRef<'a>>) -> Self {
208        Descriptors::Available(AvailableDescriptors(iter.into_iter().collect()))
209    }
210
211    /// Returns true if descriptors are available.
212    pub fn is_available(&self) -> bool {
213        matches!(self, Descriptors::Available(_))
214    }
215
216    /// Returns the available descriptors, panicking if unavailable.
217    ///
218    /// # Panics
219    /// Panics if `self` is `Unavailable`.
220    pub fn unwrap(self) -> AvailableDescriptors<'a> {
221        match self {
222            Descriptors::Available(v) => v,
223            Descriptors::Unavailable => panic!("called unwrap() on Descriptors::Unavailable"),
224        }
225    }
226
227    /// Convert to `Option<AvailableDescriptors>`, returning `None` if unavailable.
228    pub fn into_available(self) -> Option<AvailableDescriptors<'a>> {
229        match self {
230            Descriptors::Available(v) => Some(v),
231            Descriptors::Unavailable => None,
232        }
233    }
234
235    /// Apply a transformation to each descriptor ref. Preserves Unavailable.
236    pub fn map_available(self, f: impl FnMut(DescriptorRef<'a>) -> DescriptorRef<'a>) -> Self {
237        match self {
238            Descriptors::Available(a) => {
239                let mapped: SmallVec<[DescriptorRef<'a>; 2]> = a.0.into_iter().map(f).collect();
240                Descriptors::Available(AvailableDescriptors(mapped))
241            }
242            Descriptors::Unavailable => Descriptors::Unavailable,
243        }
244    }
245
246    /// Chain two descriptor results. If both are `Available`, their segments are
247    /// concatenated in write order. If either is `Unavailable`, the result is
248    /// `Unavailable`.
249    pub fn chain(self, other: Descriptors<'a>) -> Self {
250        match (self, other) {
251            (Descriptors::Available(mut a), Descriptors::Available(b)) => {
252                a.0.extend(b.0);
253                Descriptors::Available(a)
254            }
255            _ => Descriptors::Unavailable,
256        }
257    }
258}
259
260/// A descriptor segment describing a contiguous group of fields in an entry's
261/// write output. Provides resolved field names, flags, shapes, and units.
262///
263/// Sinks obtain these by calling [`Entry::descriptors()`](crate::Entry::descriptors).
264/// Simple entries yield one segment; composed entries (aggregation results,
265/// entries with flattened children) yield multiple segments in write order.
266///
267/// # Example
268///
269/// ```ignore
270/// for desc in entry.descriptors() {
271///     for field in desc.fields() {
272///         let name_parts = field.name_parts(); // prefixes then base name
273///         let base = field.base_name();        // just the field name
274///         let flags = field.flags();           // resolved flags
275///         let shape = field.shape();
276///         let unit = field.unit();
277///     }
278/// }
279/// ```
280#[derive(Clone, Debug)]
281pub struct DescriptorRef<'a> {
282    descriptor: &'a EntryDescriptor,
283    id: DescriptorId,
284    prefixes: SmallVec<[&'static str; 1]>,
285    style_index: u8,
286    extra_flags: &'static [FieldFlag],
287}
288
289impl<'a> DescriptorRef<'a> {
290    /// Create a `DescriptorRef` from a `&'static EntryDescriptor`.
291    #[doc(hidden)]
292    pub fn from_static(
293        descriptor: &'static EntryDescriptor,
294        style_index: u8,
295    ) -> DescriptorRef<'static> {
296        let id = DescriptorId::compute(descriptor, &[]);
297        DescriptorRef {
298            descriptor,
299            id,
300            prefixes: SmallVec::new(),
301            style_index,
302            extra_flags: &[],
303        }
304    }
305
306    /// Add a prefix to be prepended to all field names in this segment.
307    /// Multiple calls stack (outermost prefix first).
308    #[doc(hidden)]
309    pub fn with_prefix(mut self, prefix: &'static str) -> Self {
310        self.prefixes.insert(0, prefix);
311        self.id = DescriptorId::compute(self.descriptor, &self.prefixes);
312        self
313    }
314
315    /// Add extra flags to all fields in this segment (from flatten-site `default_flags`).
316    /// These are merged with each field's own flags at access time, respecting
317    /// field-level skips (flags in a field's `skipped_flags` are never added).
318    #[doc(hidden)]
319    pub fn with_extra_flags(mut self, flags: &'static [FieldFlag]) -> Self {
320        self.extra_flags = flags;
321        self
322    }
323
324    /// Stable identity for caching. Incorporates the base descriptor and any modifiers.
325    pub fn id(&self) -> DescriptorId {
326        self.id
327    }
328
329    /// Canonical name of this entry type.
330    pub fn name(&self) -> &str {
331        self.descriptor.name
332    }
333
334    /// Number of fields in this descriptor segment.
335    pub fn fields_len(&self) -> usize {
336        self.descriptor.fields.len()
337    }
338
339    /// The canonical timestamp field, if the entry has one.
340    pub fn timestamp(&self) -> Option<&TimestampDescriptor> {
341        self.descriptor.timestamp.as_ref()
342    }
343
344    /// Iterate over fields as [`FieldView`]s with all modifiers applied.
345    pub fn fields(&self) -> impl Iterator<Item = FieldView<'_>> {
346        (0..self.descriptor.fields.len()).map(move |i| FieldView { desc: self, idx: i })
347    }
348}
349
350/// A view of a single field with modifiers applied.
351#[derive(Clone, Debug)]
352pub struct FieldView<'a> {
353    desc: &'a DescriptorRef<'a>,
354    idx: usize,
355}
356
357impl<'a> FieldView<'a> {
358    /// Name parts in order: prefixes (outermost first) then base name.
359    /// Concatenate to get the full resolved field name.
360    pub fn name_parts(&self) -> impl Iterator<Item = &str> {
361        self.desc.prefixes.iter().copied().chain(std::iter::once(
362            self.desc.descriptor.fields[self.idx].names[self.desc.style_index as usize],
363        ))
364    }
365
366    /// Just the base field name without any prefixes.
367    ///
368    /// Guaranteed to return `&'static str` regardless of how the descriptor is
369    /// stored internally. Safe to cache or use as a long-lived key.
370    pub fn base_name(&self) -> &'static str {
371        self.desc.descriptor.fields[self.idx].names[self.desc.style_index as usize]
372    }
373    /// Flags applied to this field, including any extra flags from flatten-site defaults.
374    /// Field-level skips take precedence: flags in the field's `skipped_flags` are never included.
375    pub fn flags(&self) -> impl Iterator<Item = &'a FieldFlag> {
376        let field = &self.desc.descriptor.fields[self.idx];
377        let skipped = field.skipped_flags;
378        field.flags.iter().chain(
379            self.desc
380                .extra_flags
381                .iter()
382                .filter(move |ef| !skipped.iter().any(|s| s.type_id() == ef.type_id())),
383        )
384    }
385
386    /// Shape of this field.
387    pub fn shape(&self) -> FieldShape<'a> {
388        self.desc.descriptor.fields[self.idx].shape
389    }
390
391    /// Unit of this field.
392    pub fn unit(&self) -> Option<Unit> {
393        self.desc.descriptor.fields[self.idx].unit
394    }
395}
396
397/// Opaque identifier for a descriptor segment, stable within a process lifetime.
398///
399/// Intended for caching and deduplication by sinks. Two `DescriptorRef`s backed by
400/// the same static with the same modifiers produce equal ids. Collisions are
401/// theoretically possible (weak hash) but extremely unlikely in practice.
402///
403/// For a single cache key covering an entire entry (all segments), combine the
404/// sequence of ids from `entry.descriptors()`.
405#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
406pub struct DescriptorId(u64);
407
408impl DescriptorId {
409    // TODO: consider using fxhash instead to be a bit more collision resistant
410    fn compute(descriptor: &EntryDescriptor, prefixes: &[&'static str]) -> Self {
411        let mut id = descriptor as *const EntryDescriptor as u64;
412        for p in prefixes {
413            id = id.wrapping_mul(31).wrapping_add(p.as_ptr() as u64);
414        }
415        DescriptorId(id)
416    }
417}
418
419/// The closed/emitted shape of a field.
420#[non_exhaustive]
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub enum FieldShape<'a> {
423    /// A known scalar shape.
424    Known(KnownShape),
425    /// An optional wrapper around an inner shape.
426    Optional(ShapeRef<'a>),
427    /// A dynamic-key map.
428    Flex {
429        /// The key shape.
430        key: StringShape,
431        /// The value shape.
432        value: ShapeRef<'a>,
433    },
434    /// A list/sequence.
435    List(ShapeRef<'a>),
436    /// Shape not statically known.
437    Opaque,
438}
439
440/// Known scalar shapes.
441#[non_exhaustive]
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
443pub enum KnownShape {
444    /// Boolean
445    Bool,
446    /// Unsigned 8-bit integer
447    U8,
448    /// Unsigned 16-bit integer
449    U16,
450    /// Unsigned 32-bit integer
451    U32,
452    /// Unsigned 64-bit integer
453    U64,
454    /// Signed 8-bit integer.
455    I8,
456    /// Signed 16-bit integer.
457    I16,
458    /// Signed 32-bit integer.
459    I32,
460    /// Signed 64-bit integer.
461    I64,
462    /// 32-bit floating point
463    F32,
464    /// 64-bit floating point
465    F64,
466    /// String
467    String,
468    /// Byte slice.
469    Bytes,
470}
471
472/// String shape variants for map keys.
473#[non_exhaustive]
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
475pub enum StringShape {
476    /// Standard string.
477    String,
478}
479
480/// Opaque handle to a nested [`FieldShape`].
481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub struct ShapeRef<'a> {
483    inner: &'a FieldShape<'a>,
484}
485
486impl<'a> ShapeRef<'a> {
487    /// Borrow the underlying shape.
488    pub fn get(&self) -> &FieldShape<'a> {
489        self.inner
490    }
491
492    /// Create a ShapeRef wrapping an inner FieldShape.
493    pub const fn new(inner: &'a FieldShape<'a>) -> Self {
494        Self { inner }
495    }
496}
497
498/// A resolved field flag representing a `FlagConstructor` applied to a field.
499///
500/// Identifies a flag applied to a field, storing both the `TypeId` for identity
501/// comparison and a constructor for obtaining the flag's runtime value.
502///
503/// # Dylib caveat
504///
505/// `TypeId` is not guaranteed stable across separately compiled shared libraries.
506/// If your application loads metrique-using code via `dlopen`, flag identity checks
507/// may not work across the dylib boundary. This is a Rust language limitation, not
508/// specific to metrique.
509///
510/// # Examples
511///
512/// Sinks check for specific flags using [`is`](Self::is):
513///
514/// ```ignore
515/// use my_format::flags::HighStorageResolution;
516///
517/// for field in descriptor.fields() {
518///     if field.flags().any(|f| f.is::<HighStorageResolution>()) {
519///         // this field has high storage resolution
520///     }
521/// }
522/// ```
523///
524/// Sinks that need the flag's runtime data can call [`construct`](Self::construct):
525///
526/// ```ignore
527/// for flag in field.flags() {
528///     let metric_flags = flag.construct();
529///     // use metric_flags for format-specific behavior
530/// }
531/// ```
532#[derive(Debug)]
533pub struct FieldFlag {
534    type_id: TypeId,
535    construct: fn() -> crate::value::MetricFlags<'static>,
536}
537
538impl FieldFlag {
539    /// Create a flag from a `FlagConstructor` type.
540    pub const fn new<T: crate::value::FlagConstructor + 'static>() -> Self {
541        Self {
542            type_id: TypeId::of::<T>(),
543            construct: T::construct,
544        }
545    }
546
547    /// The [`TypeId`] of the `FlagConstructor` type.
548    pub fn type_id(&self) -> TypeId {
549        self.type_id
550    }
551
552    /// Check if this flag matches a specific `FlagConstructor` type.
553    pub fn is<T: 'static>(&self) -> bool {
554        self.type_id == TypeId::of::<T>()
555    }
556
557    /// Construct the [`MetricFlags`](crate::value::MetricFlags) value for this flag.
558    ///
559    /// This calls the `FlagConstructor::construct()` method of the original flag type,
560    /// giving sinks access to the flag's runtime data directly from the descriptor
561    /// without requiring the write path.
562    pub fn construct(&self) -> crate::value::MetricFlags<'static> {
563        (self.construct)()
564    }
565}
566
567/// Builder for [`EntryDescriptor`].
568pub struct EntryDescriptorBuilder {
569    name: &'static str,
570    fields: &'static [FieldDescriptor],
571    timestamp: Option<TimestampDescriptor>,
572}
573
574impl EntryDescriptorBuilder {
575    /// Set the timestamp descriptor.
576    pub const fn timestamp(mut self, ts: TimestampDescriptor) -> Self {
577        self.timestamp = Some(ts);
578        self
579    }
580
581    /// Set the timestamp from an Option
582    pub const fn maybe_timestamp(mut self, ts: Option<TimestampDescriptor>) -> Self {
583        self.timestamp = ts;
584        self
585    }
586
587    /// Build the [`EntryDescriptor`].
588    pub const fn build(self) -> EntryDescriptor {
589        EntryDescriptor {
590            name: self.name,
591            fields: self.fields,
592            timestamp: self.timestamp,
593        }
594    }
595}
596
597/// Builder for [`FieldDescriptor`].
598pub struct FieldDescriptorBuilder {
599    names: [&'static str; Styles::COUNT],
600    flags: &'static [FieldFlag],
601    skipped_flags: &'static [FieldFlag],
602    shape: FieldShape<'static>,
603    unit: Option<Unit>,
604}
605
606impl FieldDescriptorBuilder {
607    /// Set the PascalCase name for this field.
608    pub const fn pascal(mut self, name: &'static str) -> Self {
609        self.names[Styles::PASCAL.index as usize] = name;
610        self
611    }
612
613    /// Set the snake_case name for this field.
614    pub const fn snake(mut self, name: &'static str) -> Self {
615        self.names[Styles::SNAKE.index as usize] = name;
616        self
617    }
618
619    /// Set the kebab-case name for this field.
620    pub const fn kebab(mut self, name: &'static str) -> Self {
621        self.names[Styles::KEBAB.index as usize] = name;
622        self
623    }
624
625    /// Set the SCREAMING_SNAKE_CASE name for this field.
626    pub const fn screaming_snake(mut self, name: &'static str) -> Self {
627        self.names[Styles::SCREAMING_SNAKE.index as usize] = name;
628        self
629    }
630
631    /// Set the flags for this field.
632    pub const fn flags(mut self, flags: &'static [FieldFlag]) -> Self {
633        self.flags = flags;
634        self
635    }
636
637    /// Set the skipped flags for this field. These flags were explicitly opted out
638    /// at field level and will not be added by flatten-site `default_flags`.
639    pub const fn skipped_flags(mut self, flags: &'static [FieldFlag]) -> Self {
640        self.skipped_flags = flags;
641        self
642    }
643
644    /// Set the shape for this field.
645    pub const fn shape(mut self, shape: FieldShape<'static>) -> Self {
646        self.shape = shape;
647        self
648    }
649
650    /// Set the unit for this field.
651    pub const fn unit(mut self, unit: Unit) -> Self {
652        self.unit = Some(unit);
653        self
654    }
655
656    /// Set the unit from an Option
657    pub const fn maybe_unit(mut self, unit: Option<Unit>) -> Self {
658        self.unit = unit;
659        self
660    }
661
662    /// Build the [`FieldDescriptor`].
663    pub const fn build(self) -> FieldDescriptor {
664        FieldDescriptor {
665            names: self.names,
666            flags: self.flags,
667            skipped_flags: self.skipped_flags,
668            shape: self.shape,
669            unit: self.unit,
670        }
671    }
672}
673
674// Static assert: the number of named style setters on FieldDescriptorBuilder
675// (pascal, snake, kebab = 3, plus the base preserve slot = 4) must match Styles::COUNT.
676// If you add a new Style to Styles::ALL, add a corresponding builder method.
677const _: () = assert!(
678    Styles::COUNT == 5,
679    "Styles::COUNT changed; update FieldDescriptorBuilder with a new style method"
680);
681#[cfg(test)]
682mod tests {
683    use super::*;
684
685    #[test]
686    fn descriptor_ref_stable_id() {
687        static DESC: EntryDescriptor = EntryDescriptor::builder("Test", &[]).build();
688        let r1 = DescriptorRef::from_static(&DESC, 0);
689        let r2 = DescriptorRef::from_static(&DESC, 0);
690        assert_eq!(r1.id(), r2.id());
691        assert_eq!(r1.name(), "Test");
692    }
693
694    #[test]
695    fn different_descriptors_different_ids() {
696        static A: EntryDescriptor = EntryDescriptor::builder("A", &[]).build();
697        static B: EntryDescriptor = EntryDescriptor::builder("B", &[]).build();
698        assert_ne!(
699            DescriptorRef::from_static(&A, 0).id(),
700            DescriptorRef::from_static(&B, 0).id()
701        );
702    }
703
704    #[test]
705    fn prefix_changes_id() {
706        static DESC: EntryDescriptor = EntryDescriptor::builder("T", &[]).build();
707        let plain = DescriptorRef::from_static(&DESC, 0);
708        let prefixed = DescriptorRef::from_static(&DESC, 0).with_prefix("Api");
709        assert_ne!(plain.id(), prefixed.id());
710    }
711
712    #[test]
713    fn field_name_no_prefix() {
714        static FIELDS: [FieldDescriptor; 1] = [FieldDescriptor::builder("MyField").build()];
715        static DESC: EntryDescriptor = EntryDescriptor::builder("T", &FIELDS).build();
716
717        let d = DescriptorRef::from_static(&DESC, 0);
718        assert_eq!(d.fields().next().unwrap().base_name(), "MyField");
719    }
720
721    #[test]
722    fn field_name_with_prefix() {
723        static FIELDS: [FieldDescriptor; 1] = [FieldDescriptor::builder("Latency").build()];
724        static DESC: EntryDescriptor = EntryDescriptor::builder("T", &FIELDS).build();
725
726        let d = DescriptorRef::from_static(&DESC, 0).with_prefix("Api");
727        let fields: Vec<_> = d.fields().collect();
728        let parts: Vec<&str> = fields[0].name_parts().collect();
729        assert_eq!(parts, vec!["Api", "Latency"]);
730    }
731
732    #[test]
733    fn field_name_with_nested_prefixes() {
734        static FIELDS: [FieldDescriptor; 1] = [FieldDescriptor::builder("Latency").build()];
735        static DESC: EntryDescriptor = EntryDescriptor::builder("T", &FIELDS).build();
736
737        // Simulates: inner child applies "Api", then outer parent applies "Http"
738        let d = DescriptorRef::from_static(&DESC, 0)
739            .with_prefix("Api")
740            .with_prefix("Http");
741        let fields: Vec<_> = d.fields().collect();
742        let parts: Vec<&str> = fields[0].name_parts().collect();
743        assert_eq!(parts, vec!["Http", "Api", "Latency"]);
744    }
745
746    #[test]
747    fn field_view_iteration() {
748        static FIELDS: [FieldDescriptor; 2] = [
749            FieldDescriptor::builder("Alpha").build(),
750            FieldDescriptor::builder("Beta").unit(Unit::Count).build(),
751        ];
752        static DESC: EntryDescriptor = EntryDescriptor::builder("T", &FIELDS).build();
753
754        let d = DescriptorRef::from_static(&DESC, 0);
755        let fields: Vec<_> = d.fields().collect();
756        assert_eq!(fields.len(), 2);
757        assert_eq!(fields[0].base_name(), "Alpha");
758        assert_eq!(fields[1].base_name(), "Beta");
759        assert_eq!(fields[1].unit(), Some(Unit::Count));
760    }
761
762    #[test]
763    fn timestamp() {
764        static DESC: EntryDescriptor = EntryDescriptor::builder("E", &[])
765            .timestamp(TimestampDescriptor::new("ts"))
766            .build();
767        let d = DescriptorRef::from_static(&DESC, 0);
768        assert_eq!(d.timestamp().unwrap().name(), "ts");
769    }
770
771    #[test]
772    fn hand_written_entry_empty() {
773        use crate::{Entry, EntryWriter};
774        struct HandWritten;
775        impl Entry for HandWritten {
776            fn write<'a>(&'a self, _w: &mut impl EntryWriter<'a>) {}
777        }
778        assert_eq!(HandWritten.descriptors().is_available(), false);
779    }
780
781    #[test]
782    fn boxentry_forwards() {
783        use crate::{BoxEntry, Entry, EntryWriter};
784        static DESC: EntryDescriptor = EntryDescriptor::builder("X", &[]).build();
785        struct WithDesc;
786        impl Entry for WithDesc {
787            fn write<'a>(&'a self, _w: &mut impl EntryWriter<'a>) {}
788            fn descriptors(&self) -> Descriptors<'_> {
789                Descriptors::available(std::iter::once(DescriptorRef::from_static(&DESC, 0)))
790            }
791        }
792        let boxed = BoxEntry::new(WithDesc);
793        let descs = boxed.descriptors().unwrap();
794        assert_eq!(descs.len(), 1);
795        assert_eq!(descs[0].name(), "X");
796    }
797}