191 lines
5.0 KiB
Rust
191 lines
5.0 KiB
Rust
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<T>`.
|
|
/// This provides routing metadata (project type, timestamp) without
|
|
/// coupling the event payload itself to those concerns.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AxonEnvelope<T> {
|
|
/// 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<T: Serialize + for<'de> Deserialize<'de>> AxonEnvelope<T> {
|
|
/// 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<String, serde_json::Error> {
|
|
serde_json::to_string(self)
|
|
}
|
|
|
|
/// Deserialise an envelope from a JSON string.
|
|
pub fn from_json(s: &str) -> Result<Self, serde_json::Error> {
|
|
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<Ping> = 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");
|
|
}
|
|
}
|