lnmp_envelope/
metadata.rs

1//! Operational metadata for LNMP records
2
3use std::collections::HashMap;
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8/// Operational metadata fields for an LNMP envelope
9///
10/// All fields are optional to provide flexibility. Applications should
11/// set fields based on their requirements:
12/// - Timestamp: For temporal reasoning and freshness
13/// - Source: For routing, multi-tenant, and trust scoring
14/// - TraceID: For distributed tracing integration
15/// - Sequence: For conflict resolution and ordering
16#[derive(Debug, Clone, PartialEq, Default)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub struct EnvelopeMetadata {
19    /// Event timestamp in milliseconds since Unix epoch (UTC)
20    ///
21    /// Recommended for all events. Used for:
22    /// - Temporal ordering
23    /// - Freshness/decay calculations
24    /// - Event replay
25    pub timestamp: Option<u64>,
26
27    /// Source service/device/tenant identifier
28    ///
29    /// Examples: "auth-service", "sensor-12", "tenant-acme"
30    ///
31    /// Recommendation: Keep ≤ 64 characters
32    pub source: Option<String>,
33
34    /// Distributed tracing correlation ID
35    ///
36    /// Compatible with W3C Trace Context and OpenTelemetry.
37    /// Can hold full traceparent or just trace-id portion.
38    ///
39    /// Recommendation: Keep ≤ 128 characters
40    pub trace_id: Option<String>,
41
42    /// Monotonically increasing sequence number
43    ///
44    /// Used for ordering and conflict resolution.
45    /// Should increment for each version of the same entity.
46    pub sequence: Option<u64>,
47
48    /// Extensibility labels (reserved for future use)
49    ///
50    /// V1: Optional, implementations may ignore
51    /// Future: tenant, environment, region, priority, etc.
52    pub labels: HashMap<String, String>,
53}
54
55impl EnvelopeMetadata {
56    /// Creates a new empty metadata instance
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// Returns true if all fields are None/empty
62    pub fn is_empty(&self) -> bool {
63        self.timestamp.is_none()
64            && self.source.is_none()
65            && self.trace_id.is_none()
66            && self.sequence.is_none()
67            && self.labels.is_empty()
68    }
69
70    /// Validates metadata constraints
71    ///
72    /// Checks:
73    /// - Source length ≤ 64 characters (warning threshold)
74    /// - TraceID length ≤ 128 characters (warning threshold)
75    pub fn validate(&self) -> crate::Result<()> {
76        if let Some(ref source) = self.source {
77            if source.len() > 256 {
78                return Err(crate::EnvelopeError::StringTooLong(
79                    "source".to_string(),
80                    256,
81                ));
82            }
83        }
84
85        if let Some(ref trace_id) = self.trace_id {
86            if trace_id.len() > 256 {
87                return Err(crate::EnvelopeError::StringTooLong(
88                    "trace_id".to_string(),
89                    256,
90                ));
91            }
92        }
93
94        Ok(())
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_new_metadata_is_empty() {
104        let meta = EnvelopeMetadata::new();
105        assert!(meta.is_empty());
106    }
107
108    #[test]
109    fn test_metadata_with_timestamp_not_empty() {
110        let mut meta = EnvelopeMetadata::new();
111        meta.timestamp = Some(1732373147000);
112        assert!(!meta.is_empty());
113    }
114
115    #[test]
116    fn test_validate_accepts_short_strings() {
117        let mut meta = EnvelopeMetadata::new();
118        meta.source = Some("short-service".to_string());
119        meta.trace_id = Some("abc-123-xyz".to_string());
120        assert!(meta.validate().is_ok());
121    }
122
123    #[test]
124    fn test_validate_rejects_too_long_source() {
125        let mut meta = EnvelopeMetadata::new();
126        meta.source = Some("x".repeat(257));
127        assert!(meta.validate().is_err());
128    }
129
130    #[test]
131    fn test_validate_rejects_too_long_trace_id() {
132        let mut meta = EnvelopeMetadata::new();
133        meta.trace_id = Some("y".repeat(257));
134        assert!(meta.validate().is_err());
135    }
136}