Skip to main content

api_bones/
traceparent.rs

1//! W3C Trace Context types: `TraceId`, `SpanId`, and `TraceContext`.
2//!
3//! This module implements the
4//! [W3C Trace Context Level 1](https://www.w3.org/TR/trace-context/) spec,
5//! covering the `traceparent` header format:
6//!
7//! ```text
8//! traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
9//!              ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^
10//!              version  trace-id (32 hex)          span-id (16 hex) flags
11//! ```
12//!
13//! # Example
14//!
15//! ```rust
16//! use api_bones::traceparent::{TraceContext, SamplingFlags};
17//!
18//! let tc: TraceContext = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
19//!     .parse()
20//!     .unwrap();
21//!
22//! assert!(tc.flags.is_sampled());
23//! assert_eq!(tc.to_string(),
24//!     "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
25//! ```
26
27#[cfg(all(not(feature = "std"), feature = "alloc"))]
28use alloc::string::{String, ToString};
29use core::{fmt, str::FromStr};
30#[cfg(feature = "serde")]
31use serde::{Deserialize, Deserializer, Serialize, Serializer};
32use thiserror::Error;
33
34// ---------------------------------------------------------------------------
35// Errors
36// ---------------------------------------------------------------------------
37
38/// Error returned when parsing a `traceparent` header fails.
39#[derive(Debug, Clone, PartialEq, Eq, Error)]
40pub enum TraceContextError {
41    /// The overall format is wrong (wrong number of fields, wrong lengths, etc.).
42    #[error("invalid traceparent format")]
43    InvalidFormat,
44    /// The version byte is not supported (only `00` is currently valid).
45    #[error("unsupported traceparent version: must be \"00\"")]
46    UnsupportedVersion,
47    /// The trace-id field is all zeros, which the spec forbids.
48    #[error("trace-id must not be all zeros")]
49    ZeroTraceId,
50    /// The span-id field is all zeros, which the spec forbids.
51    #[error("span-id must not be all zeros")]
52    ZeroSpanId,
53}
54
55// ---------------------------------------------------------------------------
56// TraceId
57// ---------------------------------------------------------------------------
58
59/// A 128-bit W3C trace identifier, encoded as 32 lowercase hex characters.
60///
61/// The all-zeros value is invalid per the W3C spec and will never be produced
62/// by [`TraceId::new`] or accepted by [`TraceId::from_str`].
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
65#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
66pub struct TraceId([u8; 16]);
67
68impl TraceId {
69    /// Generate a new random `TraceId` (backed by UUID v4 bytes).
70    ///
71    /// ```rust
72    /// use api_bones::traceparent::TraceId;
73    ///
74    /// let id = TraceId::new();
75    /// assert!(!id.is_zero());
76    /// ```
77    #[must_use]
78    pub fn new() -> Self {
79        Self(*uuid::Uuid::new_v4().as_bytes())
80    }
81
82    /// Construct from raw bytes.
83    ///
84    /// Returns `None` if the bytes are all zero (invalid per W3C spec).
85    #[must_use]
86    pub fn from_bytes(bytes: [u8; 16]) -> Option<Self> {
87        if bytes == [0u8; 16] {
88            None
89        } else {
90            Some(Self(bytes))
91        }
92    }
93
94    /// Return the raw bytes.
95    #[must_use]
96    pub fn as_bytes(&self) -> &[u8; 16] {
97        &self.0
98    }
99
100    /// Returns `true` if all bytes are zero (invalid, but possible via unsafe
101    /// construction paths).
102    #[must_use]
103    pub fn is_zero(&self) -> bool {
104        self.0 == [0u8; 16]
105    }
106
107    /// Encode as a 32-character lowercase hex string.
108    #[must_use]
109    pub fn to_hex(&self) -> String {
110        self.to_string()
111    }
112}
113
114impl Default for TraceId {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120impl fmt::Display for TraceId {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        for b in &self.0 {
123            write!(f, "{b:02x}")?;
124        }
125        Ok(())
126    }
127}
128
129impl FromStr for TraceId {
130    type Err = TraceContextError;
131
132    fn from_str(s: &str) -> Result<Self, Self::Err> {
133        if s.len() != 32 {
134            return Err(TraceContextError::InvalidFormat);
135        }
136        let mut bytes = [0u8; 16];
137        for (i, b) in bytes.iter_mut().enumerate() {
138            *b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16)
139                .map_err(|_| TraceContextError::InvalidFormat)?;
140        }
141        if bytes == [0u8; 16] {
142            return Err(TraceContextError::ZeroTraceId);
143        }
144        Ok(Self(bytes))
145    }
146}
147
148#[cfg(feature = "serde")]
149impl Serialize for TraceId {
150    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
151        serializer.serialize_str(&self.to_string())
152    }
153}
154
155#[cfg(feature = "serde")]
156impl<'de> Deserialize<'de> for TraceId {
157    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
158        let s = String::deserialize(deserializer)?;
159        s.parse().map_err(serde::de::Error::custom)
160    }
161}
162
163// ---------------------------------------------------------------------------
164// SpanId
165// ---------------------------------------------------------------------------
166
167/// A 64-bit W3C span identifier, encoded as 16 lowercase hex characters.
168///
169/// The all-zeros value is invalid per the W3C spec and will never be produced
170/// by [`SpanId::new`] or accepted by [`SpanId::from_str`].
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
172#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
173#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
174pub struct SpanId([u8; 8]);
175
176impl SpanId {
177    /// Generate a new random `SpanId`.
178    ///
179    /// ```rust
180    /// use api_bones::traceparent::SpanId;
181    ///
182    /// let id = SpanId::new();
183    /// assert!(!id.is_zero());
184    /// ```
185    #[must_use]
186    pub fn new() -> Self {
187        // Use the first 8 bytes of a UUID v4 for randomness.
188        let uuid = uuid::Uuid::new_v4();
189        let b = uuid.as_bytes();
190        let mut arr = [0u8; 8];
191        arr.copy_from_slice(&b[..8]);
192        // Extremely unlikely to be all-zero, but ensure it.
193        if arr == [0u8; 8] {
194            arr[0] = 1;
195        }
196        Self(arr)
197    }
198
199    /// Construct from raw bytes.
200    ///
201    /// Returns `None` if the bytes are all zero (invalid per W3C spec).
202    #[must_use]
203    pub fn from_bytes(bytes: [u8; 8]) -> Option<Self> {
204        if bytes == [0u8; 8] {
205            None
206        } else {
207            Some(Self(bytes))
208        }
209    }
210
211    /// Return the raw bytes.
212    #[must_use]
213    pub fn as_bytes(&self) -> &[u8; 8] {
214        &self.0
215    }
216
217    /// Returns `true` if all bytes are zero.
218    #[must_use]
219    pub fn is_zero(&self) -> bool {
220        self.0 == [0u8; 8]
221    }
222
223    /// Encode as a 16-character lowercase hex string.
224    #[must_use]
225    pub fn to_hex(&self) -> String {
226        self.to_string()
227    }
228}
229
230impl Default for SpanId {
231    fn default() -> Self {
232        Self::new()
233    }
234}
235
236impl fmt::Display for SpanId {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        for b in &self.0 {
239            write!(f, "{b:02x}")?;
240        }
241        Ok(())
242    }
243}
244
245impl FromStr for SpanId {
246    type Err = TraceContextError;
247
248    fn from_str(s: &str) -> Result<Self, Self::Err> {
249        if s.len() != 16 {
250            return Err(TraceContextError::InvalidFormat);
251        }
252        let mut bytes = [0u8; 8];
253        for (i, b) in bytes.iter_mut().enumerate() {
254            *b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16)
255                .map_err(|_| TraceContextError::InvalidFormat)?;
256        }
257        if bytes == [0u8; 8] {
258            return Err(TraceContextError::ZeroSpanId);
259        }
260        Ok(Self(bytes))
261    }
262}
263
264#[cfg(feature = "serde")]
265impl Serialize for SpanId {
266    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
267        serializer.serialize_str(&self.to_string())
268    }
269}
270
271#[cfg(feature = "serde")]
272impl<'de> Deserialize<'de> for SpanId {
273    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
274        let s = String::deserialize(deserializer)?;
275        s.parse().map_err(serde::de::Error::custom)
276    }
277}
278
279// ---------------------------------------------------------------------------
280// SamplingFlags
281// ---------------------------------------------------------------------------
282
283/// W3C Trace Context sampling flags byte.
284///
285/// Currently only the `sampled` flag (bit 0) is defined by the spec.
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
287#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
288#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
289#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
290pub struct SamplingFlags(u8);
291
292impl SamplingFlags {
293    /// The `sampled` flag — bit 0 of the flags byte.
294    pub const SAMPLED: u8 = 0x01;
295
296    /// Create flags from a raw byte.
297    #[must_use]
298    pub const fn from_byte(b: u8) -> Self {
299        Self(b)
300    }
301
302    /// Create flags with the `sampled` flag set.
303    #[must_use]
304    pub const fn sampled() -> Self {
305        Self(Self::SAMPLED)
306    }
307
308    /// Create flags with no bits set (not sampled).
309    #[must_use]
310    pub const fn not_sampled() -> Self {
311        Self(0x00)
312    }
313
314    /// Returns `true` when the `sampled` flag is set.
315    #[must_use]
316    pub const fn is_sampled(&self) -> bool {
317        self.0 & Self::SAMPLED != 0
318    }
319
320    /// Return the raw flags byte.
321    #[must_use]
322    pub const fn as_byte(&self) -> u8 {
323        self.0
324    }
325}
326
327impl fmt::Display for SamplingFlags {
328    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329        write!(f, "{:02x}", self.0)
330    }
331}
332
333// ---------------------------------------------------------------------------
334// TraceContext
335// ---------------------------------------------------------------------------
336
337/// A parsed W3C `traceparent` header value.
338///
339/// Holds a [`TraceId`], a [`SpanId`], and [`SamplingFlags`]. Only spec version
340/// `00` is accepted; future versions with extra fields will be rejected.
341///
342/// # Parsing
343///
344/// ```rust
345/// use api_bones::traceparent::{TraceContext, SamplingFlags};
346///
347/// let tc: TraceContext =
348///     "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
349///         .parse()
350///         .unwrap();
351///
352/// assert!(tc.flags.is_sampled());
353/// ```
354///
355/// # Serialization
356///
357/// `Display` produces the canonical `traceparent` string which can be used
358/// directly as an HTTP header value.
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
360#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
361#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
362pub struct TraceContext {
363    /// 128-bit trace identifier.
364    pub trace_id: TraceId,
365    /// 64-bit parent span identifier.
366    pub span_id: SpanId,
367    /// Sampling and other flags.
368    pub flags: SamplingFlags,
369}
370
371impl TraceContext {
372    /// Create a new `TraceContext` with fresh random IDs and the `sampled` flag
373    /// set.
374    ///
375    /// ```rust
376    /// use api_bones::traceparent::TraceContext;
377    ///
378    /// let tc = TraceContext::new();
379    /// assert!(tc.flags.is_sampled());
380    /// ```
381    #[must_use]
382    pub fn new() -> Self {
383        Self {
384            trace_id: TraceId::new(),
385            span_id: SpanId::new(),
386            flags: SamplingFlags::sampled(),
387        }
388    }
389
390    /// Create a new child span — same `trace_id`, new `span_id`.
391    ///
392    /// ```rust
393    /// use api_bones::traceparent::TraceContext;
394    ///
395    /// let parent = TraceContext::new();
396    /// let child = parent.child_span();
397    /// assert_eq!(child.trace_id, parent.trace_id);
398    /// assert_ne!(child.span_id, parent.span_id);
399    /// ```
400    #[must_use]
401    pub fn child_span(&self) -> Self {
402        Self {
403            trace_id: self.trace_id,
404            span_id: SpanId::new(),
405            flags: self.flags,
406        }
407    }
408
409    /// Produce the canonical `traceparent` header value string.
410    ///
411    /// Equivalent to `self.to_string()`.
412    #[must_use]
413    pub fn header_value(&self) -> String {
414        self.to_string()
415    }
416
417    /// The canonical HTTP header name: `traceparent`.
418    ///
419    /// ```rust
420    /// use api_bones::traceparent::TraceContext;
421    ///
422    /// let tc = TraceContext::new();
423    /// assert_eq!(tc.header_name(), "traceparent");
424    /// ```
425    #[must_use]
426    pub fn header_name(&self) -> &'static str {
427        "traceparent"
428    }
429}
430
431// ---------------------------------------------------------------------------
432// HeaderId trait impl
433// ---------------------------------------------------------------------------
434
435#[cfg(feature = "std")]
436impl crate::header_id::HeaderId for TraceContext {
437    const HEADER_NAME: &'static str = "traceparent";
438
439    fn as_str(&self) -> std::borrow::Cow<'_, str> {
440        std::borrow::Cow::Owned(self.to_string())
441    }
442}
443
444#[cfg(all(not(feature = "std"), feature = "alloc"))]
445impl crate::header_id::HeaderId for TraceContext {
446    const HEADER_NAME: &'static str = "traceparent";
447
448    fn as_str(&self) -> alloc::borrow::Cow<'_, str> {
449        alloc::borrow::Cow::Owned(self.to_string())
450    }
451}
452
453// ---------------------------------------------------------------------------
454
455impl Default for TraceContext {
456    fn default() -> Self {
457        Self::new()
458    }
459}
460
461impl fmt::Display for TraceContext {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        write!(f, "00-{}-{}-{}", self.trace_id, self.span_id, self.flags)
464    }
465}
466
467impl FromStr for TraceContext {
468    type Err = TraceContextError;
469
470    /// Parse a `traceparent` header value.
471    ///
472    /// Only version `00` is accepted. Extra fields beyond the four standard
473    /// ones are rejected per spec (future-version compatibility is the
474    /// caller's responsibility).
475    fn from_str(s: &str) -> Result<Self, Self::Err> {
476        let parts: [&str; 4] = {
477            let mut iter = s.splitn(5, '-');
478            let version = iter.next().ok_or(TraceContextError::InvalidFormat)?;
479            let trace_id = iter.next().ok_or(TraceContextError::InvalidFormat)?;
480            let span_id = iter.next().ok_or(TraceContextError::InvalidFormat)?;
481            let flags = iter.next().ok_or(TraceContextError::InvalidFormat)?;
482            // For version 00 there must be no further fields.
483            if version == "00" && iter.next().is_some() {
484                return Err(TraceContextError::InvalidFormat);
485            }
486            [version, trace_id, span_id, flags]
487        };
488
489        if parts[0] != "00" {
490            return Err(TraceContextError::UnsupportedVersion);
491        }
492
493        let trace_id: TraceId = parts[1].parse()?;
494        let span_id: SpanId = parts[2].parse()?;
495
496        if parts[3].len() != 2 {
497            return Err(TraceContextError::InvalidFormat);
498        }
499        let flags_byte =
500            u8::from_str_radix(parts[3], 16).map_err(|_| TraceContextError::InvalidFormat)?;
501        let flags = SamplingFlags::from_byte(flags_byte);
502
503        Ok(Self {
504            trace_id,
505            span_id,
506            flags,
507        })
508    }
509}
510
511#[cfg(feature = "serde")]
512impl Serialize for TraceContext {
513    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
514        serializer.serialize_str(&self.to_string())
515    }
516}
517
518#[cfg(feature = "serde")]
519impl<'de> Deserialize<'de> for TraceContext {
520    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
521        let s = String::deserialize(deserializer)?;
522        s.parse().map_err(serde::de::Error::custom)
523    }
524}
525
526// ---------------------------------------------------------------------------
527// Tests
528// ---------------------------------------------------------------------------
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533
534    const SAMPLE: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
535
536    // --- TraceId ---
537
538    #[test]
539    fn trace_id_new_not_zero() {
540        assert!(!TraceId::new().is_zero());
541    }
542
543    #[test]
544    fn trace_id_display_is_32_hex() {
545        let id = TraceId::new();
546        let s = id.to_string();
547        assert_eq!(s.len(), 32);
548        assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
549    }
550
551    #[test]
552    fn trace_id_parse_roundtrip() {
553        let id = TraceId::new();
554        let back: TraceId = id.to_string().parse().unwrap();
555        assert_eq!(id, back);
556    }
557
558    #[test]
559    fn trace_id_parse_rejects_all_zeros() {
560        let err = "00000000000000000000000000000000"
561            .parse::<TraceId>()
562            .unwrap_err();
563        assert_eq!(err, TraceContextError::ZeroTraceId);
564    }
565
566    #[test]
567    fn trace_id_parse_rejects_wrong_length() {
568        assert!("abc".parse::<TraceId>().is_err());
569    }
570
571    #[test]
572    fn trace_id_from_bytes_rejects_zeros() {
573        assert!(TraceId::from_bytes([0u8; 16]).is_none());
574    }
575
576    // --- SpanId ---
577
578    #[test]
579    fn span_id_new_not_zero() {
580        assert!(!SpanId::new().is_zero());
581    }
582
583    #[test]
584    fn span_id_display_is_16_hex() {
585        let id = SpanId::new();
586        let s = id.to_string();
587        assert_eq!(s.len(), 16);
588        assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
589    }
590
591    #[test]
592    fn span_id_parse_roundtrip() {
593        let id = SpanId::new();
594        let back: SpanId = id.to_string().parse().unwrap();
595        assert_eq!(id, back);
596    }
597
598    #[test]
599    fn span_id_parse_rejects_all_zeros() {
600        let err = "0000000000000000".parse::<SpanId>().unwrap_err();
601        assert_eq!(err, TraceContextError::ZeroSpanId);
602    }
603
604    // --- SamplingFlags ---
605
606    #[test]
607    fn sampling_flags_sampled() {
608        let f = SamplingFlags::sampled();
609        assert!(f.is_sampled());
610        assert_eq!(f.to_string(), "01");
611    }
612
613    #[test]
614    fn sampling_flags_not_sampled() {
615        let f = SamplingFlags::not_sampled();
616        assert!(!f.is_sampled());
617        assert_eq!(f.to_string(), "00");
618    }
619
620    #[test]
621    fn sampling_flags_from_byte() {
622        assert!(SamplingFlags::from_byte(0x01).is_sampled());
623        assert!(SamplingFlags::from_byte(0x03).is_sampled()); // other bits set too
624        assert!(!SamplingFlags::from_byte(0x02).is_sampled());
625    }
626
627    // --- TraceContext ---
628
629    #[test]
630    fn parse_sample_traceparent() {
631        let tc: TraceContext = SAMPLE.parse().unwrap();
632        assert!(tc.flags.is_sampled());
633        assert_eq!(tc.to_string(), SAMPLE);
634    }
635
636    #[test]
637    fn trace_context_roundtrip() {
638        let tc = TraceContext::new();
639        let back: TraceContext = tc.to_string().parse().unwrap();
640        assert_eq!(tc, back);
641    }
642
643    #[test]
644    fn trace_context_child_span_same_trace() {
645        let parent = TraceContext::new();
646        let child = parent.child_span();
647        assert_eq!(child.trace_id, parent.trace_id);
648        assert_ne!(child.span_id, parent.span_id);
649        assert_eq!(child.flags, parent.flags);
650    }
651
652    #[test]
653    fn parse_not_sampled() {
654        let s = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00";
655        let tc: TraceContext = s.parse().unwrap();
656        assert!(!tc.flags.is_sampled());
657    }
658
659    #[test]
660    fn parse_rejects_unsupported_version() {
661        let err = "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
662            .parse::<TraceContext>()
663            .unwrap_err();
664        assert_eq!(err, TraceContextError::UnsupportedVersion);
665    }
666
667    #[test]
668    fn parse_rejects_too_few_fields() {
669        assert!(
670            "00-4bf92f3577b34da6a3ce929d0e0e4736"
671                .parse::<TraceContext>()
672                .is_err()
673        );
674    }
675
676    #[test]
677    fn parse_rejects_extra_fields_for_version_00() {
678        let s = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-extra";
679        assert!(s.parse::<TraceContext>().is_err());
680    }
681
682    #[test]
683    fn parse_rejects_zero_trace_id() {
684        let s = "00-00000000000000000000000000000000-00f067aa0ba902b7-01";
685        assert!(s.parse::<TraceContext>().is_err());
686    }
687
688    #[test]
689    fn parse_rejects_zero_span_id() {
690        let s = "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01";
691        assert!(s.parse::<TraceContext>().is_err());
692    }
693
694    #[cfg(feature = "serde")]
695    #[test]
696    fn trace_context_serde_roundtrip() {
697        let tc: TraceContext = SAMPLE.parse().unwrap();
698        let json = serde_json::to_string(&tc).unwrap();
699        let back: TraceContext = serde_json::from_str(&json).unwrap();
700        assert_eq!(tc, back);
701    }
702
703    #[cfg(feature = "serde")]
704    #[test]
705    fn trace_id_serde_roundtrip() {
706        let id = TraceId::new();
707        let json = serde_json::to_string(&id).unwrap();
708        let back: TraceId = serde_json::from_str(&json).unwrap();
709        assert_eq!(id, back);
710    }
711
712    // --- TraceId additional coverage ---
713
714    #[test]
715    fn trace_id_from_bytes_valid() {
716        let bytes = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
717        let id = TraceId::from_bytes(bytes).unwrap();
718        assert_eq!(id.as_bytes(), &bytes);
719        assert!(!id.is_zero());
720    }
721
722    #[test]
723    fn trace_id_as_bytes_roundtrip() {
724        let id = TraceId::new();
725        let bytes = *id.as_bytes();
726        let back = TraceId::from_bytes(bytes).unwrap();
727        assert_eq!(id, back);
728    }
729
730    #[test]
731    fn trace_id_to_hex() {
732        let id = TraceId::new();
733        assert_eq!(id.to_hex(), id.to_string());
734        assert_eq!(id.to_hex().len(), 32);
735    }
736
737    #[test]
738    fn trace_id_default_not_zero() {
739        let id = TraceId::default();
740        assert!(!id.is_zero());
741    }
742
743    #[test]
744    fn trace_id_parse_rejects_invalid_hex() {
745        let err = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
746            .parse::<TraceId>()
747            .unwrap_err();
748        assert_eq!(err, TraceContextError::InvalidFormat);
749    }
750
751    // --- SpanId additional coverage ---
752
753    #[test]
754    fn span_id_from_bytes_valid() {
755        let bytes = [1u8, 2, 3, 4, 5, 6, 7, 8];
756        let id = SpanId::from_bytes(bytes).unwrap();
757        assert_eq!(id.as_bytes(), &bytes);
758        assert!(!id.is_zero());
759    }
760
761    #[test]
762    fn span_id_from_bytes_rejects_zeros() {
763        assert!(SpanId::from_bytes([0u8; 8]).is_none());
764    }
765
766    #[test]
767    fn span_id_as_bytes_roundtrip() {
768        let id = SpanId::new();
769        let bytes = *id.as_bytes();
770        let back = SpanId::from_bytes(bytes).unwrap();
771        assert_eq!(id, back);
772    }
773
774    #[test]
775    fn span_id_to_hex() {
776        let id = SpanId::new();
777        assert_eq!(id.to_hex(), id.to_string());
778        assert_eq!(id.to_hex().len(), 16);
779    }
780
781    #[test]
782    fn span_id_default_not_zero() {
783        let id = SpanId::default();
784        assert!(!id.is_zero());
785    }
786
787    #[test]
788    fn span_id_parse_rejects_wrong_length() {
789        assert!("abc".parse::<SpanId>().is_err());
790    }
791
792    #[test]
793    fn span_id_parse_rejects_invalid_hex() {
794        let err = "zzzzzzzzzzzzzzzz".parse::<SpanId>().unwrap_err();
795        assert_eq!(err, TraceContextError::InvalidFormat);
796    }
797
798    // --- SamplingFlags additional coverage ---
799
800    #[test]
801    fn sampling_flags_default_is_not_sampled() {
802        let f = SamplingFlags::default();
803        assert!(!f.is_sampled());
804        assert_eq!(f.as_byte(), 0x00);
805    }
806
807    #[test]
808    fn sampling_flags_as_byte() {
809        assert_eq!(SamplingFlags::sampled().as_byte(), 0x01);
810        assert_eq!(SamplingFlags::not_sampled().as_byte(), 0x00);
811        assert_eq!(SamplingFlags::from_byte(0xAB).as_byte(), 0xAB);
812    }
813
814    // --- TraceContext additional coverage ---
815
816    #[test]
817    fn trace_context_default_is_sampled() {
818        let tc = TraceContext::default();
819        assert!(tc.flags.is_sampled());
820        assert!(!tc.trace_id.is_zero());
821        assert!(!tc.span_id.is_zero());
822    }
823
824    #[test]
825    fn trace_context_header_value() {
826        let tc: TraceContext = SAMPLE.parse().unwrap();
827        assert_eq!(tc.header_value(), SAMPLE);
828        assert_eq!(tc.header_value(), tc.to_string());
829    }
830
831    // --- TraceContextError Display ---
832
833    #[test]
834    fn trace_context_error_display() {
835        assert_eq!(
836            TraceContextError::InvalidFormat.to_string(),
837            "invalid traceparent format"
838        );
839        assert_eq!(
840            TraceContextError::UnsupportedVersion.to_string(),
841            "unsupported traceparent version: must be \"00\""
842        );
843        assert_eq!(
844            TraceContextError::ZeroTraceId.to_string(),
845            "trace-id must not be all zeros"
846        );
847        assert_eq!(
848            TraceContextError::ZeroSpanId.to_string(),
849            "span-id must not be all zeros"
850        );
851    }
852
853    // --- TraceContext parse edge cases ---
854
855    #[test]
856    fn parse_rejects_invalid_flags_hex() {
857        let s = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-zz";
858        assert!(s.parse::<TraceContext>().is_err());
859    }
860
861    #[test]
862    fn parse_rejects_flags_wrong_length() {
863        let s = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1";
864        assert!(s.parse::<TraceContext>().is_err());
865    }
866
867    // --- SpanId serde roundtrip ---
868
869    #[cfg(feature = "serde")]
870    #[test]
871    fn span_id_serde_roundtrip() {
872        let id = SpanId::new();
873        let json = serde_json::to_string(&id).unwrap();
874        let back: SpanId = serde_json::from_str(&json).unwrap();
875        assert_eq!(id, back);
876    }
877
878    #[cfg(feature = "serde")]
879    #[test]
880    fn trace_id_serde_deserialize_error() {
881        let result: Result<TraceId, _> = serde_json::from_str("\"not-valid\"");
882        assert!(result.is_err());
883    }
884
885    #[cfg(feature = "serde")]
886    #[test]
887    fn span_id_serde_deserialize_error() {
888        let result: Result<SpanId, _> = serde_json::from_str("\"not-valid\"");
889        assert!(result.is_err());
890    }
891
892    #[cfg(feature = "serde")]
893    #[test]
894    fn trace_context_serde_deserialize_error() {
895        let result: Result<TraceContext, _> = serde_json::from_str("\"not-valid\"");
896        assert!(result.is_err());
897    }
898}