pub mod codec;
use serde::{Deserialize, Serialize};
pub use codec::{EnvelopeCodec, JsonCodec};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
pub namespace: String,
pub msg_type: String,
pub payload: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
}
impl Envelope {
pub fn new(namespace: &str, msg_type: &str, payload: serde_json::Value) -> Self {
Self {
namespace: namespace.to_string(),
msg_type: msg_type.to_string(),
payload,
timestamp: None,
from: None,
}
}
pub fn with_timestamp(mut self) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
self.timestamp = Some(now);
self
}
pub fn serialize(&self) -> Result<Vec<u8>, EnvelopeError> {
serde_json::to_vec(self).map_err(EnvelopeError::Serialization)
}
pub fn deserialize(data: &[u8]) -> Result<Self, EnvelopeError> {
serde_json::from_slice(data).map_err(EnvelopeError::Deserialization)
}
}
#[derive(Debug, thiserror::Error)]
pub enum EnvelopeError {
#[error("envelope serialization error: {0}")]
Serialization(serde_json::Error),
#[error("envelope deserialization error: {0}")]
Deserialization(serde_json::Error),
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_envelope_serialize_deserialize_roundtrip() {
let envelope = Envelope::new("ft", "offer", json!({"file": "test.txt", "size": 1024}))
.with_timestamp();
let bytes = envelope.serialize().expect("serialize");
let decoded = Envelope::deserialize(&bytes).expect("deserialize");
assert_eq!(decoded.namespace, "ft");
assert_eq!(decoded.msg_type, "offer");
assert_eq!(decoded.payload["file"], "test.txt");
assert_eq!(decoded.payload["size"], 1024);
assert!(decoded.timestamp.is_some());
assert_eq!(decoded.timestamp, envelope.timestamp);
}
#[test]
fn test_envelope_timestamp_generation() {
let before = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let envelope = Envelope::new("test", "ping", json!(null)).with_timestamp();
let after = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let ts = envelope.timestamp.expect("timestamp should be set");
assert!(ts >= before, "timestamp {ts} should be >= {before}");
assert!(ts <= after, "timestamp {ts} should be <= {after}");
}
#[test]
fn test_envelope_unknown_fields_ignored() {
let json_str = r#"{
"namespace": "chat",
"msg_type": "message",
"payload": {"text": "hello"},
"timestamp": 1234567890,
"from": "peer-1",
"future_field": "should be ignored",
"another_new_field": 42
}"#;
let envelope: Envelope =
serde_json::from_str(json_str).expect("should ignore unknown fields");
assert_eq!(envelope.namespace, "chat");
assert_eq!(envelope.msg_type, "message");
assert_eq!(envelope.payload["text"], "hello");
assert_eq!(envelope.timestamp, Some(1234567890));
assert_eq!(envelope.from.as_deref(), Some("peer-1"));
}
#[test]
fn test_envelope_empty_payload() {
let envelope = Envelope::new("bus", "heartbeat", json!(null));
let bytes = envelope.serialize().expect("serialize");
let decoded = Envelope::deserialize(&bytes).expect("deserialize");
assert_eq!(decoded.namespace, "bus");
assert_eq!(decoded.msg_type, "heartbeat");
assert!(decoded.payload.is_null());
assert!(decoded.timestamp.is_none());
assert!(decoded.from.is_none());
}
#[test]
fn test_envelope_large_payload() {
let large_data: Vec<u8> = (0..100_000).map(|i| (i % 256) as u8).collect();
let payload = json!({"data": large_data});
let envelope = Envelope::new("stream", "chunk", payload.clone()).with_timestamp();
let bytes = envelope.serialize().expect("serialize");
let decoded = Envelope::deserialize(&bytes).expect("deserialize");
assert_eq!(decoded.namespace, "stream");
assert_eq!(decoded.msg_type, "chunk");
assert_eq!(decoded.payload, payload);
}
#[test]
fn test_envelope_optional_fields_not_serialized_when_none() {
let envelope = Envelope::new("test", "msg", json!("data"));
let json_str = serde_json::to_string(&envelope).expect("serialize");
assert!(!json_str.contains("timestamp"), "None timestamp should be skipped");
assert!(!json_str.contains("from"), "None from should be skipped");
}
#[test]
fn test_envelope_deserialization_error() {
let bad_json = b"not valid json";
let result = Envelope::deserialize(bad_json);
assert!(result.is_err());
}
}