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}