Skip to main content

obs_proto/
ext.rs

1//! Ergonomic supplements for proto-generated enums.
2//!
3//! Phase 3b (obs-migration spec § 4): obs-types is retired. The seven
4//! hand-rolled Rust enums collapse into the proto-generated enums in
5//! [`crate::obs::v1`]. buffa-codegen doesn't yet emit the full
6//! ergonomic surface (`Ord`, `FromStr` with aliases, short-name
7//! constants, domain helpers like [`Severity::otlp_number`]), so this
8//! module adds them as hand-written supplements.
9//!
10//! Each enum gains:
11//!
12//! - **Short-name const aliases** — `Severity::Info`, `Tier::Log`, `Cardinality::High` —
13//!   non-wire-breaking re-exports of the canonical SCREAM_CASE variants. Keeps call sites compiling
14//!   without the long `SEVERITY_INFO` form every time.
15//! - **Ordering** — `PartialOrd + Ord` via discriminant order so `Severity::Info < Severity::Warn`
16//!   works.
17//! - **Default** — already emitted by buffa-codegen (variant 0).
18//! - **Serde** — `Serialize`/`Deserialize` by short lowercased name (`"info"`, `"warn"`) to match
19//!   `obs.yaml` config.
20//! - **FromStr** — accepts short name + proto name + aliases (`"warning"` → `Warn`, `"err"` →
21//!   `Error`). Errors via [`UnknownVariant`].
22//! - **Domain methods** — `as_str`, `otlp_number`, `cap`, `is_label_compatible`,
23//!   `is_envelope_lifted`, etc.
24//!
25//! When buffa 0.6 lands with `generate_rich`, this file shrinks to
26//! whatever the codegen doesn't cover (likely just serde config
27//! naming).
28
29use std::str::FromStr;
30
31use crate::obs::v1::{
32    Cardinality, Classification, FieldKind, MetricKind, SamplingReason, Severity, Tier,
33};
34
35/// Error returned by `FromStr` parsers when an enum variant isn't
36/// recognised. Uniform shape across every enum in this module so
37/// callers can match on one type.
38#[derive(Debug, thiserror::Error)]
39#[error("unknown {kind} variant: {value:?}")]
40pub struct UnknownVariant {
41    /// Enum name (e.g. `"Severity"`).
42    pub kind: &'static str,
43    /// Unrecognised input.
44    pub value: String,
45}
46
47// ============================================================================
48// Tier
49// ============================================================================
50
51#[allow(non_upper_case_globals)]
52impl Tier {
53    /// Short-name alias for [`Self::TIER_UNSPECIFIED`].
54    pub const Unspecified: Self = Self::TIER_UNSPECIFIED;
55    /// Short-name alias for [`Self::TIER_LOG`].
56    pub const Log: Self = Self::TIER_LOG;
57    /// Short-name alias for [`Self::TIER_METRIC`].
58    pub const Metric: Self = Self::TIER_METRIC;
59    /// Short-name alias for [`Self::TIER_TRACE`].
60    pub const Trace: Self = Self::TIER_TRACE;
61    /// Short-name alias for [`Self::TIER_AUDIT`].
62    pub const Audit: Self = Self::TIER_AUDIT;
63
64    /// Stable string label; used by sinks (`labels["tier"]`) and the
65    /// CLI when rendering.
66    #[must_use]
67    pub const fn as_str(self) -> &'static str {
68        match self {
69            Self::TIER_LOG => "log",
70            Self::TIER_METRIC => "metric",
71            Self::TIER_TRACE => "trace",
72            Self::TIER_AUDIT => "audit",
73            _ => "unspecified",
74        }
75    }
76}
77
78impl Ord for Tier {
79    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
80        (*self as i32).cmp(&(*other as i32))
81    }
82}
83
84impl PartialOrd for Tier {
85    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
86        Some(self.cmp(other))
87    }
88}
89
90impl FromStr for Tier {
91    type Err = UnknownVariant;
92
93    fn from_str(s: &str) -> Result<Self, Self::Err> {
94        match s.to_ascii_lowercase().as_str() {
95            "log" | "tier_log" => Ok(Self::TIER_LOG),
96            "metric" | "tier_metric" => Ok(Self::TIER_METRIC),
97            "trace" | "tier_trace" => Ok(Self::TIER_TRACE),
98            "audit" | "tier_audit" => Ok(Self::TIER_AUDIT),
99            "unspecified" | "tier_unspecified" => Ok(Self::TIER_UNSPECIFIED),
100            _ => Err(UnknownVariant {
101                kind: "Tier",
102                value: s.to_string(),
103            }),
104        }
105    }
106}
107
108impl serde::Serialize for Tier {
109    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
110        s.serialize_str(self.as_str())
111    }
112}
113
114impl<'de> serde::Deserialize<'de> for Tier {
115    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
116        let s = <std::string::String as serde::Deserialize>::deserialize(d)?;
117        Self::from_str(&s).map_err(serde::de::Error::custom)
118    }
119}
120
121// ============================================================================
122// Severity
123// ============================================================================
124
125#[allow(non_upper_case_globals)]
126impl Severity {
127    /// Short-name alias for [`Self::SEVERITY_UNSPECIFIED`].
128    pub const Unspecified: Self = Self::SEVERITY_UNSPECIFIED;
129    /// Short-name alias for [`Self::SEVERITY_TRACE`].
130    pub const Trace: Self = Self::SEVERITY_TRACE;
131    /// Short-name alias for [`Self::SEVERITY_DEBUG`].
132    pub const Debug: Self = Self::SEVERITY_DEBUG;
133    /// Short-name alias for [`Self::SEVERITY_INFO`].
134    pub const Info: Self = Self::SEVERITY_INFO;
135    /// Short-name alias for [`Self::SEVERITY_WARN`].
136    pub const Warn: Self = Self::SEVERITY_WARN;
137    /// Short-name alias for [`Self::SEVERITY_ERROR`].
138    pub const Error: Self = Self::SEVERITY_ERROR;
139    /// Short-name alias for [`Self::SEVERITY_FATAL`].
140    pub const Fatal: Self = Self::SEVERITY_FATAL;
141
142    /// Stable string label used in label values and CLI rendering.
143    #[must_use]
144    pub const fn as_str(self) -> &'static str {
145        match self {
146            Self::SEVERITY_TRACE => "trace",
147            Self::SEVERITY_DEBUG => "debug",
148            Self::SEVERITY_INFO => "info",
149            Self::SEVERITY_WARN => "warn",
150            Self::SEVERITY_ERROR => "error",
151            Self::SEVERITY_FATAL => "fatal",
152            _ => "unspecified",
153        }
154    }
155
156    /// Map to OTLP `SeverityNumber` (1..=24 with 4 buckets per band).
157    #[must_use]
158    pub const fn otlp_number(self) -> i32 {
159        match self {
160            Self::SEVERITY_TRACE => 1,
161            Self::SEVERITY_DEBUG => 5,
162            Self::SEVERITY_INFO => 9,
163            Self::SEVERITY_WARN => 13,
164            Self::SEVERITY_ERROR => 17,
165            Self::SEVERITY_FATAL => 21,
166            _ => 0,
167        }
168    }
169}
170
171impl Ord for Severity {
172    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
173        (*self as i32).cmp(&(*other as i32))
174    }
175}
176
177impl PartialOrd for Severity {
178    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
179        Some(self.cmp(other))
180    }
181}
182
183impl FromStr for Severity {
184    type Err = UnknownVariant;
185
186    fn from_str(s: &str) -> Result<Self, Self::Err> {
187        match s.to_ascii_lowercase().as_str() {
188            "trace" | "severity_trace" => Ok(Self::SEVERITY_TRACE),
189            "debug" | "severity_debug" => Ok(Self::SEVERITY_DEBUG),
190            "info" | "severity_info" => Ok(Self::SEVERITY_INFO),
191            "warn" | "warning" | "severity_warn" => Ok(Self::SEVERITY_WARN),
192            "error" | "err" | "severity_error" => Ok(Self::SEVERITY_ERROR),
193            "fatal" | "severity_fatal" => Ok(Self::SEVERITY_FATAL),
194            "unspecified" | "severity_unspecified" => Ok(Self::SEVERITY_UNSPECIFIED),
195            _ => Err(UnknownVariant {
196                kind: "Severity",
197                value: s.to_string(),
198            }),
199        }
200    }
201}
202
203impl serde::Serialize for Severity {
204    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
205        s.serialize_str(self.as_str())
206    }
207}
208
209impl<'de> serde::Deserialize<'de> for Severity {
210    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
211        let s = <std::string::String as serde::Deserialize>::deserialize(d)?;
212        Self::from_str(&s).map_err(serde::de::Error::custom)
213    }
214}
215
216// ============================================================================
217// FieldKind
218// ============================================================================
219
220#[allow(non_upper_case_globals)]
221impl FieldKind {
222    /// Short-name alias for `FIELD_KIND_UNSPECIFIED`.
223    pub const Unspecified: Self = Self::FIELD_KIND_UNSPECIFIED;
224    /// Short-name alias for `LABEL`.
225    pub const Label: Self = Self::LABEL;
226    /// Short-name alias for `ATTRIBUTE`.
227    pub const Attribute: Self = Self::ATTRIBUTE;
228    /// Short-name alias for `MEASUREMENT`.
229    pub const Measurement: Self = Self::MEASUREMENT;
230    /// Short-name alias for `TRACE_ID`.
231    pub const TraceId: Self = Self::TRACE_ID;
232    /// Short-name alias for `SPAN_ID`.
233    pub const SpanId: Self = Self::SPAN_ID;
234    /// Short-name alias for `PARENT_SPAN_ID`.
235    pub const ParentSpanId: Self = Self::PARENT_SPAN_ID;
236    /// Short-name alias for `TIMESTAMP_NS`.
237    pub const TimestampNs: Self = Self::TIMESTAMP_NS;
238    /// Short-name alias for `DURATION_NS`.
239    pub const DurationNs: Self = Self::DURATION_NS;
240    /// Short-name alias for `FORENSIC`.
241    pub const Forensic: Self = Self::FORENSIC;
242
243    /// Stable string label.
244    #[must_use]
245    pub const fn as_str(self) -> &'static str {
246        match self {
247            Self::LABEL => "label",
248            Self::ATTRIBUTE => "attribute",
249            Self::MEASUREMENT => "measurement",
250            Self::TRACE_ID => "trace_id",
251            Self::SPAN_ID => "span_id",
252            Self::PARENT_SPAN_ID => "parent_span_id",
253            Self::TIMESTAMP_NS => "timestamp_ns",
254            Self::DURATION_NS => "duration_ns",
255            Self::FORENSIC => "forensic",
256            _ => "unspecified",
257        }
258    }
259
260    /// True if a value of this kind is lifted from the typed payload
261    /// to a dedicated envelope slot.
262    #[must_use]
263    pub const fn is_envelope_lifted(self) -> bool {
264        matches!(
265            self,
266            Self::TRACE_ID | Self::SPAN_ID | Self::PARENT_SPAN_ID | Self::TIMESTAMP_NS
267        )
268    }
269}
270
271impl Ord for FieldKind {
272    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
273        (*self as i32).cmp(&(*other as i32))
274    }
275}
276
277impl PartialOrd for FieldKind {
278    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
279        Some(self.cmp(other))
280    }
281}
282
283impl FromStr for FieldKind {
284    type Err = UnknownVariant;
285
286    fn from_str(s: &str) -> Result<Self, Self::Err> {
287        match s.to_ascii_lowercase().as_str() {
288            "label" => Ok(Self::LABEL),
289            "attribute" => Ok(Self::ATTRIBUTE),
290            "measurement" => Ok(Self::MEASUREMENT),
291            "trace_id" => Ok(Self::TRACE_ID),
292            "span_id" => Ok(Self::SPAN_ID),
293            "parent_span_id" => Ok(Self::PARENT_SPAN_ID),
294            "timestamp_ns" => Ok(Self::TIMESTAMP_NS),
295            "duration_ns" => Ok(Self::DURATION_NS),
296            "forensic" => Ok(Self::FORENSIC),
297            "unspecified" | "field_kind_unspecified" => Ok(Self::FIELD_KIND_UNSPECIFIED),
298            _ => Err(UnknownVariant {
299                kind: "FieldKind",
300                value: s.to_string(),
301            }),
302        }
303    }
304}
305
306impl serde::Serialize for FieldKind {
307    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
308        s.serialize_str(self.as_str())
309    }
310}
311
312impl<'de> serde::Deserialize<'de> for FieldKind {
313    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
314        let s = <std::string::String as serde::Deserialize>::deserialize(d)?;
315        Self::from_str(&s).map_err(serde::de::Error::custom)
316    }
317}
318
319// ============================================================================
320// Cardinality
321// ============================================================================
322
323#[allow(non_upper_case_globals)]
324impl Cardinality {
325    /// Short-name alias for `CARDINALITY_UNSPECIFIED`.
326    pub const Unspecified: Self = Self::CARDINALITY_UNSPECIFIED;
327    /// Short-name alias for `LOW`.
328    pub const Low: Self = Self::LOW;
329    /// Short-name alias for `MEDIUM`.
330    pub const Medium: Self = Self::MEDIUM;
331    /// Short-name alias for `HIGH`.
332    pub const High: Self = Self::HIGH;
333    /// Short-name alias for `UNBOUNDED`.
334    pub const Unbounded: Self = Self::UNBOUNDED;
335
336    /// Stable string label.
337    #[must_use]
338    pub const fn as_str(self) -> &'static str {
339        match self {
340            Self::LOW => "low",
341            Self::MEDIUM => "medium",
342            Self::HIGH => "high",
343            Self::UNBOUNDED => "unbounded",
344            _ => "unspecified",
345        }
346    }
347
348    /// Numeric cap: the maximum distinct value count permitted at this
349    /// level. Returns [`u64::MAX`] for `Unbounded`.
350    #[must_use]
351    pub const fn cap(self) -> u64 {
352        match self {
353            Self::LOW => 10,
354            Self::MEDIUM => 10_000,
355            Self::HIGH => 1_000_000,
356            Self::UNBOUNDED => u64::MAX,
357            _ => 0,
358        }
359    }
360
361    /// True if this cardinality is permitted on a `FieldKind::Label`
362    /// field.
363    #[must_use]
364    pub const fn is_label_compatible(self) -> bool {
365        matches!(self, Self::LOW | Self::MEDIUM)
366    }
367
368    /// True if this cardinality is permitted on a
369    /// `FieldKind::Measurement` field.
370    #[must_use]
371    pub const fn is_measurement_compatible(self) -> bool {
372        !matches!(self, Self::UNBOUNDED)
373    }
374}
375
376impl Ord for Cardinality {
377    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
378        (*self as i32).cmp(&(*other as i32))
379    }
380}
381
382impl PartialOrd for Cardinality {
383    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
384        Some(self.cmp(other))
385    }
386}
387
388impl FromStr for Cardinality {
389    type Err = UnknownVariant;
390
391    fn from_str(s: &str) -> Result<Self, Self::Err> {
392        match s.to_ascii_lowercase().as_str() {
393            "low" => Ok(Self::LOW),
394            "medium" => Ok(Self::MEDIUM),
395            "high" => Ok(Self::HIGH),
396            "unbounded" => Ok(Self::UNBOUNDED),
397            "unspecified" | "cardinality_unspecified" => Ok(Self::CARDINALITY_UNSPECIFIED),
398            _ => Err(UnknownVariant {
399                kind: "Cardinality",
400                value: s.to_string(),
401            }),
402        }
403    }
404}
405
406impl serde::Serialize for Cardinality {
407    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
408        s.serialize_str(self.as_str())
409    }
410}
411
412impl<'de> serde::Deserialize<'de> for Cardinality {
413    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
414        let s = <std::string::String as serde::Deserialize>::deserialize(d)?;
415        Self::from_str(&s).map_err(serde::de::Error::custom)
416    }
417}
418
419// ============================================================================
420// Classification
421// ============================================================================
422
423#[allow(non_upper_case_globals)]
424impl Classification {
425    /// Short-name alias for `CLASSIFICATION_UNSPECIFIED`.
426    pub const Unspecified: Self = Self::CLASSIFICATION_UNSPECIFIED;
427    /// Short-name alias for `INTERNAL`.
428    pub const Internal: Self = Self::INTERNAL;
429    /// Short-name alias for `PII`.
430    pub const Pii: Self = Self::PII;
431    /// Short-name alias for `SECRET`.
432    pub const Secret: Self = Self::SECRET;
433
434    /// Stable string label.
435    #[must_use]
436    pub const fn as_str(self) -> &'static str {
437        match self {
438            Self::INTERNAL => "internal",
439            Self::PII => "pii",
440            Self::SECRET => "secret",
441            _ => "unspecified",
442        }
443    }
444}
445
446impl Ord for Classification {
447    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
448        (*self as i32).cmp(&(*other as i32))
449    }
450}
451
452impl PartialOrd for Classification {
453    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
454        Some(self.cmp(other))
455    }
456}
457
458impl FromStr for Classification {
459    type Err = UnknownVariant;
460
461    fn from_str(s: &str) -> Result<Self, Self::Err> {
462        match s.to_ascii_lowercase().as_str() {
463            "internal" => Ok(Self::INTERNAL),
464            "pii" => Ok(Self::PII),
465            "secret" => Ok(Self::SECRET),
466            "unspecified" | "classification_unspecified" => Ok(Self::CLASSIFICATION_UNSPECIFIED),
467            _ => Err(UnknownVariant {
468                kind: "Classification",
469                value: s.to_string(),
470            }),
471        }
472    }
473}
474
475impl serde::Serialize for Classification {
476    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
477        s.serialize_str(self.as_str())
478    }
479}
480
481impl<'de> serde::Deserialize<'de> for Classification {
482    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
483        let s = <std::string::String as serde::Deserialize>::deserialize(d)?;
484        Self::from_str(&s).map_err(serde::de::Error::custom)
485    }
486}
487
488// ============================================================================
489// MetricKind
490// ============================================================================
491
492#[allow(non_upper_case_globals)]
493impl MetricKind {
494    /// Short-name alias for `METRIC_KIND_UNSPECIFIED`.
495    pub const Unspecified: Self = Self::METRIC_KIND_UNSPECIFIED;
496    /// Short-name alias for `METRIC_KIND_COUNTER`.
497    pub const Counter: Self = Self::METRIC_KIND_COUNTER;
498    /// Short-name alias for `METRIC_KIND_GAUGE`.
499    pub const Gauge: Self = Self::METRIC_KIND_GAUGE;
500    /// Short-name alias for `METRIC_KIND_HISTOGRAM`.
501    pub const Histogram: Self = Self::METRIC_KIND_HISTOGRAM;
502
503    /// Stable string label.
504    #[must_use]
505    pub const fn as_str(self) -> &'static str {
506        match self {
507            Self::METRIC_KIND_COUNTER => "counter",
508            Self::METRIC_KIND_GAUGE => "gauge",
509            Self::METRIC_KIND_HISTOGRAM => "histogram",
510            _ => "unspecified",
511        }
512    }
513}
514
515impl Ord for MetricKind {
516    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
517        (*self as i32).cmp(&(*other as i32))
518    }
519}
520
521impl PartialOrd for MetricKind {
522    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
523        Some(self.cmp(other))
524    }
525}
526
527impl FromStr for MetricKind {
528    type Err = UnknownVariant;
529
530    fn from_str(s: &str) -> Result<Self, Self::Err> {
531        match s.to_ascii_lowercase().as_str() {
532            "counter" | "metric_kind_counter" => Ok(Self::METRIC_KIND_COUNTER),
533            "gauge" | "metric_kind_gauge" => Ok(Self::METRIC_KIND_GAUGE),
534            "histogram" | "metric_kind_histogram" => Ok(Self::METRIC_KIND_HISTOGRAM),
535            "unspecified" | "metric_kind_unspecified" => Ok(Self::METRIC_KIND_UNSPECIFIED),
536            _ => Err(UnknownVariant {
537                kind: "MetricKind",
538                value: s.to_string(),
539            }),
540        }
541    }
542}
543
544impl serde::Serialize for MetricKind {
545    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
546        s.serialize_str(self.as_str())
547    }
548}
549
550impl<'de> serde::Deserialize<'de> for MetricKind {
551    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
552        let s = <std::string::String as serde::Deserialize>::deserialize(d)?;
553        Self::from_str(&s).map_err(serde::de::Error::custom)
554    }
555}
556
557// ============================================================================
558// SamplingReason
559// ============================================================================
560
561#[allow(non_upper_case_globals)]
562impl SamplingReason {
563    /// Short-name alias for `SAMPLING_REASON_UNSPECIFIED`.
564    pub const Unspecified: Self = Self::SAMPLING_REASON_UNSPECIFIED;
565    /// Short-name alias for `SAMPLING_REASON_HEAD_RATE`.
566    pub const HeadRate: Self = Self::SAMPLING_REASON_HEAD_RATE;
567    /// Short-name alias for `SAMPLING_REASON_TAIL_ERROR`.
568    pub const TailError: Self = Self::SAMPLING_REASON_TAIL_ERROR;
569    /// Short-name alias for `SAMPLING_REASON_SLOW`.
570    pub const Slow: Self = Self::SAMPLING_REASON_SLOW;
571    /// Short-name alias for `SAMPLING_REASON_FORENSIC`.
572    pub const Forensic: Self = Self::SAMPLING_REASON_FORENSIC;
573    /// Short-name alias for `SAMPLING_REASON_AUDIT`.
574    pub const Audit: Self = Self::SAMPLING_REASON_AUDIT;
575    /// Short-name alias for `SAMPLING_REASON_RUNTIME`.
576    pub const Runtime: Self = Self::SAMPLING_REASON_RUNTIME;
577    /// Short-name alias for `SAMPLING_REASON_OVERRIDE`.
578    pub const Override: Self = Self::SAMPLING_REASON_OVERRIDE;
579
580    /// Stable string label.
581    #[must_use]
582    pub const fn as_str(self) -> &'static str {
583        match self {
584            Self::SAMPLING_REASON_HEAD_RATE => "head_rate",
585            Self::SAMPLING_REASON_TAIL_ERROR => "tail_error",
586            Self::SAMPLING_REASON_SLOW => "slow",
587            Self::SAMPLING_REASON_FORENSIC => "forensic",
588            Self::SAMPLING_REASON_AUDIT => "audit",
589            Self::SAMPLING_REASON_RUNTIME => "runtime",
590            Self::SAMPLING_REASON_OVERRIDE => "override",
591            _ => "unspecified",
592        }
593    }
594}
595
596impl Ord for SamplingReason {
597    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
598        (*self as i32).cmp(&(*other as i32))
599    }
600}
601
602impl PartialOrd for SamplingReason {
603    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
604        Some(self.cmp(other))
605    }
606}
607
608impl FromStr for SamplingReason {
609    type Err = UnknownVariant;
610
611    fn from_str(s: &str) -> Result<Self, Self::Err> {
612        match s.to_ascii_lowercase().as_str() {
613            "head_rate" => Ok(Self::SAMPLING_REASON_HEAD_RATE),
614            "tail_error" => Ok(Self::SAMPLING_REASON_TAIL_ERROR),
615            "slow" => Ok(Self::SAMPLING_REASON_SLOW),
616            "forensic" => Ok(Self::SAMPLING_REASON_FORENSIC),
617            "audit" => Ok(Self::SAMPLING_REASON_AUDIT),
618            "runtime" => Ok(Self::SAMPLING_REASON_RUNTIME),
619            "override" => Ok(Self::SAMPLING_REASON_OVERRIDE),
620            "unspecified" | "sampling_reason_unspecified" => Ok(Self::SAMPLING_REASON_UNSPECIFIED),
621            _ => Err(UnknownVariant {
622                kind: "SamplingReason",
623                value: s.to_string(),
624            }),
625        }
626    }
627}
628
629impl serde::Serialize for SamplingReason {
630    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
631        s.serialize_str(self.as_str())
632    }
633}
634
635impl<'de> serde::Deserialize<'de> for SamplingReason {
636    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
637        let s = <std::string::String as serde::Deserialize>::deserialize(d)?;
638        Self::from_str(&s).map_err(serde::de::Error::custom)
639    }
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    #[test]
647    fn test_severity_short_name_aliases() {
648        assert_eq!(Severity::Info, Severity::SEVERITY_INFO);
649        assert_eq!(Severity::Warn, Severity::SEVERITY_WARN);
650    }
651
652    #[test]
653    fn test_severity_ord_by_discriminant() {
654        assert!(Severity::Info < Severity::Warn);
655        assert!(Severity::Error < Severity::Fatal);
656    }
657
658    #[test]
659    fn test_severity_otlp_number() {
660        assert_eq!(Severity::Info.otlp_number(), 9);
661        assert_eq!(Severity::Fatal.otlp_number(), 21);
662    }
663
664    #[test]
665    fn test_severity_from_str_aliases() {
666        assert_eq!("warning".parse::<Severity>().unwrap(), Severity::Warn);
667        assert_eq!("err".parse::<Severity>().unwrap(), Severity::Error);
668        assert_eq!("INFO".parse::<Severity>().unwrap(), Severity::Info);
669        assert_eq!("SEVERITY_WARN".parse::<Severity>().unwrap(), Severity::Warn,);
670    }
671
672    #[test]
673    fn test_tier_as_str_and_from_str() {
674        assert_eq!(Tier::Log.as_str(), "log");
675        assert_eq!("AUDIT".parse::<Tier>().unwrap(), Tier::Audit);
676    }
677
678    #[test]
679    fn test_cardinality_caps_and_compat() {
680        assert_eq!(Cardinality::Low.cap(), 10);
681        assert_eq!(Cardinality::Medium.cap(), 10_000);
682        assert_eq!(Cardinality::High.cap(), 1_000_000);
683        assert_eq!(Cardinality::Unbounded.cap(), u64::MAX);
684        assert!(Cardinality::Low.is_label_compatible());
685        assert!(!Cardinality::High.is_label_compatible());
686    }
687
688    #[test]
689    fn test_field_kind_envelope_lifted() {
690        assert!(FieldKind::TraceId.is_envelope_lifted());
691        assert!(FieldKind::SpanId.is_envelope_lifted());
692        assert!(!FieldKind::Label.is_envelope_lifted());
693    }
694
695    #[test]
696    fn test_serde_roundtrip_via_short_name() {
697        let json = serde_json::to_string(&Severity::Info).unwrap();
698        assert_eq!(json, "\"info\"");
699        let back: Severity = serde_json::from_str(&json).unwrap();
700        assert_eq!(back, Severity::Info);
701    }
702}