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