lnmp_net/
message.rs

1//! Core message structure for LNMP-Net
2
3use lnmp_envelope::LnmpEnvelope;
4
5use crate::error::{NetError, Result};
6use crate::kind::MessageKind;
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11/// Network message with semantic classification and QoS metadata
12///
13/// Wraps an LNMP envelope (which contains the record + operational metadata)
14/// with network behavior metadata: message kind, priority, TTL, and optional class.
15///
16/// # Examples
17///
18/// ```
19/// use lnmp_core::{LnmpRecord, LnmpField, LnmpValue};
20/// use lnmp_envelope::EnvelopeBuilder;
21/// use lnmp_net::{MessageKind, NetMessage};
22///
23/// // Create a record
24/// let mut record = LnmpRecord::new();
25/// record.add_field(LnmpField { fid: 42, value: LnmpValue::Int(100) });
26///
27/// // Create envelope with timestamp
28/// let envelope = EnvelopeBuilder::new(record)
29///     .timestamp(1700000000000)
30///     .source("sensor-01")
31///     .build();
32///
33/// // Create network message
34/// let msg = NetMessage::new(envelope, MessageKind::Event);
35/// ```
36///
37/// Network message wrapping an LNMP envelope with network behavior metadata.
38///
39/// Combines LNMP record data (via envelope) with network-level information
40/// (kind, priority, TTL, class) for intelligent routing and LLM integration.
41#[derive(Debug, Clone)]
42#[cfg_attr(
43    feature = "serde",
44    derive(Serialize, Deserialize),
45    serde(bound(deserialize = ""))
46)]
47pub struct NetMessage {
48    /// LNMP envelope containing record and operational metadata
49    pub envelope: LnmpEnvelope,
50
51    /// Message semantic classification
52    pub kind: MessageKind,
53
54    /// Priority (0-255): 0-50 low, 51-200 normal, 201-255 critical
55    pub priority: u8,
56
57    /// Time-to-live in milliseconds
58    pub ttl_ms: u32,
59
60    /// Optional domain classification (e.g., "health", "safety", "traffic")
61    pub class: Option<String>,
62}
63
64impl NetMessage {
65    /// Creates a new network message with defaults from MessageKind
66    ///
67    /// Uses `kind.default_priority()` and `kind.default_ttl_ms()` for QoS fields.
68    pub fn new(envelope: LnmpEnvelope, kind: MessageKind) -> Self {
69        Self {
70            envelope,
71            kind,
72            priority: kind.default_priority(),
73            ttl_ms: kind.default_ttl_ms(),
74            class: None,
75        }
76    }
77
78    /// Creates a new network message with custom priority and TTL
79    pub fn with_qos(envelope: LnmpEnvelope, kind: MessageKind, priority: u8, ttl_ms: u32) -> Self {
80        Self {
81            envelope,
82            kind,
83            priority,
84            ttl_ms,
85            class: None,
86        }
87    }
88
89    /// Checks if the message has expired based on current time
90    ///
91    /// Returns `Err(NetError::MissingTimestamp)` if envelope has no timestamp.
92    ///
93    /// # Arguments
94    ///
95    /// * `now_ms` - Current time in epoch milliseconds
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use lnmp_core::LnmpRecord;
101    /// use lnmp_envelope::EnvelopeBuilder;
102    /// use lnmp_net::{MessageKind, NetMessage};
103    ///
104    /// let envelope = EnvelopeBuilder::new(LnmpRecord::new())
105    ///     .timestamp(1000)
106    ///     .build();
107    ///
108    /// let msg = NetMessage::with_qos(envelope, MessageKind::Event, 100, 5000);
109    ///
110    /// assert!(!msg.is_expired(5000).unwrap()); // Age = 4000ms, TTL = 5000ms
111    /// assert!(msg.is_expired(7000).unwrap());  // Age = 6000ms > 5000ms
112    /// ```
113    pub fn is_expired(&self, now_ms: u64) -> Result<bool> {
114        let timestamp = self
115            .envelope
116            .metadata
117            .timestamp
118            .ok_or(NetError::MissingTimestamp)?;
119
120        let age_ms = now_ms.saturating_sub(timestamp);
121        Ok(age_ms > self.ttl_ms as u64)
122    }
123
124    /// Returns the age of the message in milliseconds
125    ///
126    /// Returns `None` if envelope has no timestamp.
127    pub fn age_ms(&self, now_ms: u64) -> Option<u64> {
128        self.envelope
129            .metadata
130            .timestamp
131            .map(|ts| now_ms.saturating_sub(ts))
132    }
133
134    /// Returns the source identifier from envelope metadata
135    pub fn source(&self) -> Option<&str> {
136        self.envelope.metadata.source.as_deref()
137    }
138
139    /// Returns the trace ID from envelope metadata
140    pub fn trace_id(&self) -> Option<&str> {
141        self.envelope.metadata.trace_id.as_deref()
142    }
143
144    /// Returns the timestamp from envelope metadata
145    pub fn timestamp(&self) -> Option<u64> {
146        self.envelope.metadata.timestamp
147    }
148
149    /// Returns a reference to the underlying LNMP record
150    pub fn record(&self) -> &lnmp_core::LnmpRecord {
151        &self.envelope.record
152    }
153
154    /// Validates the message (envelope + QoS fields)
155    pub fn validate(&self) -> Result<()> {
156        self.envelope.validate()?;
157        Ok(())
158    }
159}
160
161/// Fluent builder for constructing network messages
162///
163/// # Examples
164///
165/// ```
166/// use lnmp_core::{LnmpRecord, LnmpField, LnmpValue};
167/// use lnmp_envelope::EnvelopeBuilder;
168/// use lnmp_net::{MessageKind, NetMessageBuilder};
169///
170/// let mut record = LnmpRecord::new();
171/// record.add_field(LnmpField { fid: 12, value: LnmpValue::Int(42) });
172///
173/// let envelope = EnvelopeBuilder::new(record)
174///     .timestamp(1700000000000)
175///     .source("service-a")
176///     .build();
177///
178/// let msg = NetMessageBuilder::new(envelope, MessageKind::Alert)
179///     .priority(255)
180///     .ttl_ms(1000)
181///     .class("safety")
182///     .build();
183///
184/// assert_eq!(msg.priority, 255);
185/// assert_eq!(msg.class, Some("safety".to_string()));
186/// ```
187pub struct NetMessageBuilder {
188    envelope: LnmpEnvelope,
189    kind: MessageKind,
190    priority: u8,
191    ttl_ms: u32,
192    class: Option<String>,
193}
194
195impl NetMessageBuilder {
196    /// Creates a new builder with defaults from MessageKind
197    pub fn new(envelope: LnmpEnvelope, kind: MessageKind) -> Self {
198        Self {
199            envelope,
200            kind,
201            priority: kind.default_priority(),
202            ttl_ms: kind.default_ttl_ms(),
203            class: None,
204        }
205    }
206
207    /// Sets the priority (0-255)
208    pub fn priority(mut self, priority: u8) -> Self {
209        self.priority = priority;
210        self
211    }
212
213    /// Sets the TTL in milliseconds
214    pub fn ttl_ms(mut self, ttl_ms: u32) -> Self {
215        self.ttl_ms = ttl_ms;
216        self
217    }
218
219    /// Sets the domain class
220    pub fn class(mut self, class: impl Into<String>) -> Self {
221        self.class = Some(class.into());
222        self
223    }
224
225    /// Builds the NetMessage
226    pub fn build(self) -> NetMessage {
227        NetMessage {
228            envelope: self.envelope,
229            kind: self.kind,
230            priority: self.priority,
231            ttl_ms: self.ttl_ms,
232            class: self.class,
233        }
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use lnmp_core::{LnmpField, LnmpRecord, LnmpValue};
241    use lnmp_envelope::EnvelopeBuilder;
242
243    fn sample_record() -> LnmpRecord {
244        let mut record = LnmpRecord::new();
245        record.add_field(LnmpField {
246            fid: 42,
247            value: LnmpValue::Int(100),
248        });
249        record
250    }
251
252    fn sample_envelope(timestamp: u64) -> LnmpEnvelope {
253        EnvelopeBuilder::new(sample_record())
254            .timestamp(timestamp)
255            .source("test-node")
256            .build()
257    }
258
259    #[test]
260    fn test_new_message_uses_kind_defaults() {
261        let envelope = sample_envelope(1000);
262        let msg = NetMessage::new(envelope, MessageKind::Alert);
263
264        assert_eq!(msg.kind, MessageKind::Alert);
265        assert_eq!(msg.priority, 255); // Alert default
266        assert_eq!(msg.ttl_ms, 1000); // Alert default
267    }
268
269    #[test]
270    fn test_is_expired_fresh_message() {
271        let envelope = sample_envelope(1000);
272        let msg = NetMessage::with_qos(envelope, MessageKind::Event, 100, 5000);
273
274        // Age = 4000ms, TTL = 5000ms -> not expired
275        assert!(!msg.is_expired(5000).unwrap());
276    }
277
278    #[test]
279    fn test_is_expired_old_message() {
280        let envelope = sample_envelope(1000);
281        let msg = NetMessage::with_qos(envelope, MessageKind::Event, 100, 5000);
282
283        // Age = 6000ms, TTL = 5000ms -> expired
284        assert!(msg.is_expired(7000).unwrap());
285    }
286
287    #[test]
288    fn test_is_expired_missing_timestamp() {
289        let envelope = LnmpEnvelope::new(sample_record());
290        let msg = NetMessage::new(envelope, MessageKind::Event);
291
292        assert!(msg.is_expired(5000).is_err());
293    }
294
295    #[test]
296    fn test_age_ms() {
297        let envelope = sample_envelope(1000);
298        let msg = NetMessage::new(envelope, MessageKind::Event);
299
300        assert_eq!(msg.age_ms(6000), Some(5000));
301    }
302
303    #[test]
304    fn test_age_ms_missing_timestamp() {
305        let envelope = LnmpEnvelope::new(sample_record());
306        let msg = NetMessage::new(envelope, MessageKind::Event);
307
308        assert_eq!(msg.age_ms(5000), None);
309    }
310
311    #[test]
312    fn test_accessors() {
313        let envelope = EnvelopeBuilder::new(sample_record())
314            .timestamp(1000)
315            .source("node-alpha")
316            .trace_id("trace-123")
317            .build();
318
319        let msg = NetMessage::new(envelope, MessageKind::State);
320
321        assert_eq!(msg.source(), Some("node-alpha"));
322        assert_eq!(msg.trace_id(), Some("trace-123"));
323        assert_eq!(msg.timestamp(), Some(1000));
324    }
325
326    #[test]
327    fn test_builder_defaults() {
328        let envelope = sample_envelope(1000);
329        let msg = NetMessageBuilder::new(envelope, MessageKind::Command).build();
330
331        assert_eq!(msg.priority, 150); // Command default
332        assert_eq!(msg.ttl_ms, 2000); // Command default
333        assert_eq!(msg.class, None);
334    }
335
336    #[test]
337    fn test_builder_custom_values() {
338        let envelope = sample_envelope(1000);
339        let msg = NetMessageBuilder::new(envelope, MessageKind::Event)
340            .priority(200)
341            .ttl_ms(10000)
342            .class("health")
343            .build();
344
345        assert_eq!(msg.priority, 200);
346        assert_eq!(msg.ttl_ms, 10000);
347        assert_eq!(msg.class, Some("health".to_string()));
348    }
349
350    #[test]
351    fn test_validate() {
352        let envelope = sample_envelope(1000);
353        let msg = NetMessage::new(envelope, MessageKind::Event);
354
355        assert!(msg.validate().is_ok());
356    }
357}