use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::project::ProjectType; /// Opaque unique identifier for an event. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct EventId(pub Uuid); impl EventId { /// Generate a new random event ID. pub fn new() -> Self { Self(Uuid::new_v4()) } } impl Default for EventId { fn default() -> Self { Self::new() } } impl std::fmt::Display for EventId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } /// Milliseconds since the Unix epoch. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct Timestamp(pub i64); impl Timestamp { /// Current time as a millisecond timestamp. pub fn now() -> Self { use std::time::{SystemTime, UNIX_EPOCH}; let ms = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("system clock before epoch") .as_millis() as i64; Self(ms) } /// Construct from a raw millisecond value. pub fn from_ms(ms: i64) -> Self { Self(ms) } /// Return the raw millisecond value. pub fn as_ms(self) -> i64 { self.0 } } impl Default for Timestamp { fn default() -> Self { Self::now() } } /// Diagnostic / compile-error severity level. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum Severity { #[default] Info, Warning, Error, } /// Universal envelope that wraps every typed event. /// /// Services never send raw events — they always send an `AxonEnvelope`. /// This provides routing metadata (project type, timestamp) without /// coupling the event payload itself to those concerns. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AxonEnvelope { /// Unique ID for this event instance. pub event_id: EventId, /// Which project emitted this event. pub project_type: ProjectType, /// When the event was created (ms since epoch). pub timestamp: Timestamp, /// The typed event payload. pub payload: T, } impl Deserialize<'de>> AxonEnvelope { /// Wrap a payload in an envelope for the given project type. pub fn new(project_type: ProjectType, payload: T) -> Self { Self { event_id: EventId::new(), project_type, timestamp: Timestamp::now(), payload, } } /// Serialise this envelope to a JSON string. pub fn to_json(&self) -> Result { serde_json::to_string(self) } /// Deserialise an envelope from a JSON string. pub fn from_json(s: &str) -> Result { serde_json::from_str(s) } } #[cfg(test)] mod tests { use super::*; use crate::project::ProjectType; #[test] fn event_id_is_unique() { let a = EventId::new(); let b = EventId::new(); assert_ne!(a, b); } #[test] fn event_id_default_is_unique() { let a = EventId::default(); let b = EventId::default(); assert_ne!(a, b); } #[test] fn timestamp_now_is_positive() { let t = Timestamp::now(); assert!(t.as_ms() > 0); } #[test] fn timestamp_roundtrip() { let t = Timestamp::from_ms(1_700_000_000_000); let json = serde_json::to_string(&t).unwrap(); let back: Timestamp = serde_json::from_str(&json).unwrap(); assert_eq!(back.as_ms(), 1_700_000_000_000); } #[test] fn severity_serde_roundtrip() { for s in [Severity::Info, Severity::Warning, Severity::Error] { let json = serde_json::to_string(&s).unwrap(); let back: Severity = serde_json::from_str(&json).unwrap(); assert_eq!(back, s); } } #[test] fn severity_default_is_info() { assert_eq!(Severity::default(), Severity::Info); } #[test] fn axon_envelope_roundtrip() { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] struct Ping { msg: String, } let payload = Ping { msg: "hello".into() }; let env = AxonEnvelope::new(ProjectType::NeuronRs, payload); let json = env.to_json().unwrap(); let back: AxonEnvelope = AxonEnvelope::from_json(&json).unwrap(); assert_eq!(back.project_type, ProjectType::NeuronRs); assert_eq!(back.payload.msg, "hello"); assert_eq!(back.event_id, env.event_id); } #[test] fn axon_envelope_timestamp_is_recent() { #[derive(Debug, Clone, Serialize, Deserialize)] struct Empty; let env = AxonEnvelope::new(ProjectType::Engram, Empty); let now = Timestamp::now().as_ms(); // Should be within 5 seconds assert!(now - env.timestamp.as_ms() < 5_000); } #[test] fn event_id_display() { let id = EventId(Uuid::nil()); assert_eq!(id.to_string(), "00000000-0000-0000-0000-000000000000"); } }