Skip to main content

brainwires_agent_network/network/
envelope.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// A message envelope that wraps any payload with routing metadata.
6///
7/// This is the universal message format used across all transports.
8/// Transports serialize/deserialize envelopes to their wire format.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct MessageEnvelope {
11    /// Unique message identifier.
12    pub id: Uuid,
13    /// The sender's agent identity UUID.
14    pub sender: Uuid,
15    /// Who this message is addressed to.
16    pub recipient: MessageTarget,
17    /// The message payload.
18    pub payload: Payload,
19    /// When the message was created.
20    pub timestamp: DateTime<Utc>,
21    /// Optional time-to-live (number of hops before the message is dropped).
22    pub ttl: Option<u32>,
23    /// Optional correlation ID for request-response patterns.
24    pub correlation_id: Option<Uuid>,
25}
26
27impl MessageEnvelope {
28    /// Create a new envelope addressed to a specific agent.
29    pub fn direct(sender: Uuid, recipient: Uuid, payload: impl Into<Payload>) -> Self {
30        Self {
31            id: Uuid::new_v4(),
32            sender,
33            recipient: MessageTarget::Direct(recipient),
34            payload: payload.into(),
35            timestamp: Utc::now(),
36            ttl: None,
37            correlation_id: None,
38        }
39    }
40
41    /// Create a new broadcast envelope.
42    pub fn broadcast(sender: Uuid, payload: impl Into<Payload>) -> Self {
43        Self {
44            id: Uuid::new_v4(),
45            sender,
46            recipient: MessageTarget::Broadcast,
47            payload: payload.into(),
48            timestamp: Utc::now(),
49            ttl: None,
50            correlation_id: None,
51        }
52    }
53
54    /// Create a new topic-addressed envelope.
55    pub fn topic(sender: Uuid, topic: impl Into<String>, payload: impl Into<Payload>) -> Self {
56        Self {
57            id: Uuid::new_v4(),
58            sender,
59            recipient: MessageTarget::Topic(topic.into()),
60            payload: payload.into(),
61            timestamp: Utc::now(),
62            ttl: None,
63            correlation_id: None,
64        }
65    }
66
67    /// Set the TTL on this envelope.
68    pub fn with_ttl(mut self, ttl: u32) -> Self {
69        self.ttl = Some(ttl);
70        self
71    }
72
73    /// Set a correlation ID for request-response tracking.
74    pub fn with_correlation(mut self, correlation_id: Uuid) -> Self {
75        self.correlation_id = Some(correlation_id);
76        self
77    }
78
79    /// Create a reply envelope to this message.
80    pub fn reply(&self, sender: Uuid, payload: impl Into<Payload>) -> Self {
81        Self {
82            id: Uuid::new_v4(),
83            sender,
84            recipient: MessageTarget::Direct(self.sender),
85            payload: payload.into(),
86            timestamp: Utc::now(),
87            ttl: None,
88            correlation_id: Some(self.id),
89        }
90    }
91}
92
93/// The target of a message.
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
95pub enum MessageTarget {
96    /// Send to a specific agent by UUID.
97    Direct(Uuid),
98    /// Send to all known peers.
99    Broadcast,
100    /// Send to all agents subscribed to a topic.
101    Topic(String),
102}
103
104/// The payload of a message.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub enum Payload {
107    /// Structured JSON data.
108    Json(serde_json::Value),
109    /// Raw binary data (base64-encoded in JSON serialization).
110    #[serde(with = "base64_bytes")]
111    Binary(Vec<u8>),
112    /// Plain text.
113    Text(String),
114}
115
116impl From<serde_json::Value> for Payload {
117    fn from(v: serde_json::Value) -> Self {
118        Payload::Json(v)
119    }
120}
121
122impl From<String> for Payload {
123    fn from(s: String) -> Self {
124        Payload::Text(s)
125    }
126}
127
128impl From<&str> for Payload {
129    fn from(s: &str) -> Self {
130        Payload::Text(s.to_string())
131    }
132}
133
134impl From<Vec<u8>> for Payload {
135    fn from(b: Vec<u8>) -> Self {
136        Payload::Binary(b)
137    }
138}
139
140/// Serde helper for base64-encoding binary payloads in JSON.
141mod base64_bytes {
142    use base64::Engine;
143    use base64::engine::general_purpose::STANDARD;
144    use serde::{Deserialize, Deserializer, Serialize, Serializer};
145
146    pub fn serialize<S: Serializer>(bytes: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
147        STANDARD.encode(bytes).serialize(s)
148    }
149
150    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
151        let encoded = String::deserialize(d)?;
152        STANDARD.decode(&encoded).map_err(serde::de::Error::custom)
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn direct_envelope_fields() {
162        let sender = Uuid::new_v4();
163        let recipient = Uuid::new_v4();
164        let env = MessageEnvelope::direct(sender, recipient, "hello");
165
166        assert_eq!(env.sender, sender);
167        assert_eq!(env.recipient, MessageTarget::Direct(recipient));
168        assert!(env.ttl.is_none());
169        assert!(env.correlation_id.is_none());
170    }
171
172    #[test]
173    fn broadcast_envelope() {
174        let sender = Uuid::new_v4();
175        let env = MessageEnvelope::broadcast(sender, "ping");
176        assert_eq!(env.recipient, MessageTarget::Broadcast);
177    }
178
179    #[test]
180    fn topic_envelope() {
181        let sender = Uuid::new_v4();
182        let env = MessageEnvelope::topic(sender, "status-updates", "agent online");
183        assert_eq!(env.recipient, MessageTarget::Topic("status-updates".into()));
184    }
185
186    #[test]
187    fn reply_sets_correlation() {
188        let sender_a = Uuid::new_v4();
189        let sender_b = Uuid::new_v4();
190        let original = MessageEnvelope::direct(sender_a, sender_b, "request");
191        let reply = original.reply(sender_b, "response");
192
193        assert_eq!(reply.sender, sender_b);
194        assert_eq!(reply.recipient, MessageTarget::Direct(sender_a));
195        assert_eq!(reply.correlation_id, Some(original.id));
196    }
197
198    #[test]
199    fn with_ttl() {
200        let env = MessageEnvelope::broadcast(Uuid::new_v4(), "test").with_ttl(5);
201        assert_eq!(env.ttl, Some(5));
202    }
203
204    #[test]
205    fn envelope_serde_roundtrip() {
206        let env = MessageEnvelope::direct(Uuid::new_v4(), Uuid::new_v4(), "hello");
207        let json = serde_json::to_string(&env).unwrap();
208        let deserialized: MessageEnvelope = serde_json::from_str(&json).unwrap();
209        assert_eq!(deserialized.id, env.id);
210        assert_eq!(deserialized.sender, env.sender);
211    }
212
213    #[test]
214    fn binary_payload_serde_roundtrip() {
215        let env =
216            MessageEnvelope::direct(Uuid::new_v4(), Uuid::new_v4(), vec![0xDE, 0xAD, 0xBE, 0xEF]);
217        let json = serde_json::to_string(&env).unwrap();
218        let deserialized: MessageEnvelope = serde_json::from_str(&json).unwrap();
219        match deserialized.payload {
220            Payload::Binary(bytes) => assert_eq!(bytes, vec![0xDE, 0xAD, 0xBE, 0xEF]),
221            _ => panic!("expected Binary payload"),
222        }
223    }
224
225    #[test]
226    fn json_payload_from_value() {
227        let payload: Payload = serde_json::json!({"key": "value"}).into();
228        match payload {
229            Payload::Json(v) => assert_eq!(v["key"], "value"),
230            _ => panic!("expected Json payload"),
231        }
232    }
233}