shepherd-launcher/crates/shepherd-api/src/events.rs
2026-02-08 14:01:49 -05:00

126 lines
3.4 KiB
Rust

//! Event types for shepherdd -> client streaming
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use shepherd_util::{EntryId, SessionId};
use std::time::Duration;
use crate::{API_VERSION, ServiceStateSnapshot, SessionEndReason, WarningSeverity};
/// Event envelope
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub api_version: u32,
pub timestamp: DateTime<Local>,
pub payload: EventPayload,
}
impl Event {
pub fn new(payload: EventPayload) -> Self {
Self {
api_version: API_VERSION,
timestamp: shepherd_util::now(),
payload,
}
}
}
/// All possible events from the service to clients
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum EventPayload {
/// Full state snapshot (sent on subscribe and major changes)
StateChanged(ServiceStateSnapshot),
/// Session has started
SessionStarted {
session_id: SessionId,
entry_id: EntryId,
label: String,
/// Deadline for session. None means unlimited.
deadline: Option<DateTime<Local>>,
},
/// Warning issued for current session
WarningIssued {
session_id: SessionId,
threshold_seconds: u64,
time_remaining: Duration,
severity: WarningSeverity,
message: Option<String>,
},
/// Session is expiring (termination initiated)
SessionExpiring { session_id: SessionId },
/// Session has ended
SessionEnded {
session_id: SessionId,
entry_id: EntryId,
reason: SessionEndReason,
duration: Duration,
},
/// Policy was reloaded
PolicyReloaded { entry_count: usize },
/// Entry availability changed (for UI updates)
EntryAvailabilityChanged { entry_id: EntryId, enabled: bool },
/// Volume status changed
VolumeChanged { percent: u8, muted: bool },
/// Service is shutting down
Shutdown,
/// Audit event (for admin clients)
AuditEntry {
event_type: String,
details: serde_json::Value,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_serialization() {
let event = Event::new(EventPayload::SessionStarted {
session_id: SessionId::new(),
entry_id: EntryId::new("game-1"),
label: "Test Game".into(),
deadline: Some(shepherd_util::now()),
});
let json = serde_json::to_string(&event).unwrap();
let parsed: Event = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.api_version, API_VERSION);
assert!(matches!(
parsed.payload,
EventPayload::SessionStarted { .. }
));
}
#[test]
fn event_serialization_unlimited() {
// Test with unlimited session (deadline=None)
let event = Event::new(EventPayload::SessionStarted {
session_id: SessionId::new(),
entry_id: EntryId::new("game-1"),
label: "Unlimited Game".into(),
deadline: None,
});
let json = serde_json::to_string(&event).unwrap();
let parsed: Event = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.api_version, API_VERSION);
if let EventPayload::SessionStarted { deadline, .. } = parsed.payload {
assert!(deadline.is_none());
} else {
panic!("Expected SessionStarted");
}
}
}