lnmp_envelope/
envelope.rs

1//! LNMP Envelope wrapper for records with operational metadata
2
3use lnmp_core::LnmpRecord;
4
5use crate::metadata::EnvelopeMetadata;
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10/// Complete LNMP message with operational context
11///
12/// An envelope wraps an LNMP record with operational metadata
13/// (timestamp, source, trace_id, sequence) without affecting
14/// the record's semantic checksum or deterministic properties.
15///
16/// # Example
17///
18/// ```
19/// use lnmp_core::{LnmpRecord, LnmpField, LnmpValue};
20/// use lnmp_envelope::{LnmpEnvelope, EnvelopeMetadata};
21///
22/// let mut record = LnmpRecord::new();
23/// record.add_field(LnmpField { fid: 12, value: LnmpValue::Int(14532) });
24///
25/// let mut metadata = EnvelopeMetadata::new();
26/// metadata.timestamp = Some(1732373147000);
27/// metadata.source = Some("auth-service".to_string());
28///
29/// let envelope = LnmpEnvelope::with_metadata(record, metadata);
30/// ```
31#[derive(Debug, Clone, PartialEq)]
32#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
33pub struct LnmpEnvelope {
34    /// The LNMP record (mandatory)
35    pub record: LnmpRecord,
36
37    /// Optional operational metadata
38    pub metadata: EnvelopeMetadata,
39}
40
41impl LnmpEnvelope {
42    /// Creates a new envelope with the given record and empty metadata
43    pub fn new(record: LnmpRecord) -> Self {
44        Self {
45            record,
46            metadata: EnvelopeMetadata::new(),
47        }
48    }
49
50    /// Creates a new envelope with the given record and metadata
51    pub fn with_metadata(record: LnmpRecord, metadata: EnvelopeMetadata) -> Self {
52        Self { record, metadata }
53    }
54
55    /// Returns true if metadata is empty
56    pub fn has_metadata(&self) -> bool {
57        !self.metadata.is_empty()
58    }
59
60    /// Validates the envelope (record and metadata)
61    pub fn validate(&self) -> crate::Result<()> {
62        self.metadata.validate()?;
63        Ok(())
64    }
65}
66
67/// Fluent builder for constructing envelopes
68///
69/// # Example
70///
71/// ```
72/// use lnmp_core::{LnmpRecord, LnmpField, LnmpValue};
73/// use lnmp_envelope::EnvelopeBuilder;
74///
75/// let mut record = LnmpRecord::new();
76/// record.add_field(LnmpField { fid: 12, value: LnmpValue::Int(14532) });
77///
78/// let envelope = EnvelopeBuilder::new(record)
79///     .timestamp(1732373147000)
80///     .source("auth-service")
81///     .trace_id("abc-123-xyz")
82///     .sequence(42)
83///     .build();
84/// ```
85pub struct EnvelopeBuilder {
86    record: LnmpRecord,
87    metadata: EnvelopeMetadata,
88}
89
90impl EnvelopeBuilder {
91    /// Creates a new builder with the given record
92    pub fn new(record: LnmpRecord) -> Self {
93        Self {
94            record,
95            metadata: EnvelopeMetadata::new(),
96        }
97    }
98
99    /// Sets the timestamp (Unix epoch milliseconds, UTC)
100    pub fn timestamp(mut self, ts: u64) -> Self {
101        self.metadata.timestamp = Some(ts);
102        self
103    }
104
105    /// Sets the source identifier
106    pub fn source(mut self, src: impl Into<String>) -> Self {
107        self.metadata.source = Some(src.into());
108        self
109    }
110
111    /// Sets the trace ID for distributed tracing
112    pub fn trace_id(mut self, id: impl Into<String>) -> Self {
113        self.metadata.trace_id = Some(id.into());
114        self
115    }
116
117    /// Sets the sequence number
118    pub fn sequence(mut self, seq: u64) -> Self {
119        self.metadata.sequence = Some(seq);
120        self
121    }
122
123    /// Adds a label (key-value pair)
124    pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
125        self.metadata.labels.insert(key.into(), value.into());
126        self
127    }
128
129    /// Builds the envelope
130    pub fn build(self) -> LnmpEnvelope {
131        LnmpEnvelope::with_metadata(self.record, self.metadata)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use lnmp_core::{LnmpField, LnmpValue};
139
140    fn sample_record() -> LnmpRecord {
141        let mut record = LnmpRecord::new();
142        record.add_field(LnmpField {
143            fid: 12,
144            value: LnmpValue::Int(14532),
145        });
146        record
147    }
148
149    #[test]
150    fn test_new_envelope_has_no_metadata() {
151        let envelope = LnmpEnvelope::new(sample_record());
152        assert!(!envelope.has_metadata());
153    }
154
155    #[test]
156    fn test_builder_basic() {
157        let envelope = EnvelopeBuilder::new(sample_record())
158            .timestamp(1732373147000)
159            .source("test-service")
160            .build();
161
162        assert!(envelope.has_metadata());
163        assert_eq!(envelope.metadata.timestamp, Some(1732373147000));
164        assert_eq!(envelope.metadata.source, Some("test-service".to_string()));
165    }
166
167    #[test]
168    fn test_builder_all_fields() {
169        let envelope = EnvelopeBuilder::new(sample_record())
170            .timestamp(1732373147000)
171            .source("auth-service")
172            .trace_id("abc-123-xyz")
173            .sequence(42)
174            .label("tenant", "acme")
175            .label("env", "prod")
176            .build();
177
178        assert_eq!(envelope.metadata.timestamp, Some(1732373147000));
179        assert_eq!(envelope.metadata.source, Some("auth-service".to_string()));
180        assert_eq!(envelope.metadata.trace_id, Some("abc-123-xyz".to_string()));
181        assert_eq!(envelope.metadata.sequence, Some(42));
182        assert_eq!(envelope.metadata.labels.len(), 2);
183    }
184
185    #[test]
186    fn test_validate_succeeds_for_valid_envelope() {
187        let envelope = EnvelopeBuilder::new(sample_record())
188            .timestamp(1732373147000)
189            .source("short")
190            .build();
191
192        assert!(envelope.validate().is_ok());
193    }
194}