add axon-events: typed event schema for the full neuron-technologies stack

This commit is contained in:
2026-04-27 20:01:39 -05:00
parent 745278c902
commit 7a81bb11a7
17 changed files with 2115 additions and 36 deletions
+190
View File
@@ -0,0 +1,190 @@
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");
}
}