Archived
add axon-events: typed event schema for the full neuron-technologies stack
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user