#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum EventKind {
TargetLoaded,
TargetUnloaded,
ParamChanged,
ExportStarted,
ExportFinished,
PluginRegistered,
Error,
Custom(String),
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Event {
pub kind: EventKind,
pub payload: String,
pub timestamp_ms: u64,
}
pub type EventHandler = Box<dyn Fn(&Event) + Send + Sync>;
pub struct EventBus {
handlers: Vec<(EventKind, EventHandler)>,
history: Vec<Event>,
}
impl EventBus {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
handlers: Vec::new(),
history: Vec::new(),
}
}
#[allow(dead_code)]
pub fn subscribe(&mut self, kind: EventKind, handler: EventHandler) {
self.handlers.push((kind, handler));
}
#[allow(dead_code)]
pub fn publish(&mut self, event: Event) {
for (kind, handler) in &self.handlers {
if *kind == event.kind {
handler(&event);
}
}
self.history.push(event);
}
#[allow(dead_code)]
pub fn history(&self) -> &[Event] {
&self.history
}
#[allow(dead_code)]
pub fn clear_history(&mut self) {
self.history.clear();
}
#[allow(dead_code)]
pub fn handler_count(&self) -> usize {
self.handlers.len()
}
#[allow(dead_code)]
pub fn event_count(&self) -> usize {
self.history.len()
}
}
impl Default for EventBus {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
pub fn make_param_changed_event(name: &str, value: f32) -> Event {
Event {
kind: EventKind::ParamChanged,
payload: format!(r#"{{"name":"{name}","value":{value}}}"#),
timestamp_ms: 0,
}
}
#[allow(dead_code)]
pub fn make_export_event(path: &str, format: &str) -> Event {
Event {
kind: EventKind::ExportStarted,
payload: format!(r#"{{"path":"{path}","format":"{format}"}}"#),
timestamp_ms: 0,
}
}
#[allow(dead_code)]
pub fn make_error_event(msg: &str) -> Event {
Event {
kind: EventKind::Error,
payload: format!(r#"{{"message":"{msg}"}}"#),
timestamp_ms: 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
fn ts() -> u64 {
0
}
#[test]
fn test_subscribe_and_publish_triggers_handler() {
let counter = Arc::new(Mutex::new(0u32));
let c = Arc::clone(&counter);
let mut bus = EventBus::new();
bus.subscribe(
EventKind::TargetLoaded,
Box::new(move |_| {
*c.lock().expect("should succeed") += 1;
}),
);
bus.publish(Event {
kind: EventKind::TargetLoaded,
payload: "null".to_string(),
timestamp_ms: ts(),
});
assert_eq!(*counter.lock().expect("should succeed"), 1);
}
#[test]
fn test_wrong_kind_does_not_trigger() {
let counter = Arc::new(Mutex::new(0u32));
let c = Arc::clone(&counter);
let mut bus = EventBus::new();
bus.subscribe(
EventKind::ExportFinished,
Box::new(move |_| {
*c.lock().expect("should succeed") += 1;
}),
);
bus.publish(Event {
kind: EventKind::TargetLoaded,
payload: "null".to_string(),
timestamp_ms: ts(),
});
assert_eq!(*counter.lock().expect("should succeed"), 0);
}
#[test]
fn test_history_grows() {
let mut bus = EventBus::new();
for i in 0..5 {
bus.publish(Event {
kind: EventKind::ParamChanged,
payload: format!("{i}"),
timestamp_ms: ts(),
});
}
assert_eq!(bus.history().len(), 5);
}
#[test]
fn test_clear_history_empties() {
let mut bus = EventBus::new();
bus.publish(Event {
kind: EventKind::Error,
payload: "null".to_string(),
timestamp_ms: ts(),
});
assert!(!bus.history().is_empty());
bus.clear_history();
assert!(bus.history().is_empty());
}
#[test]
fn test_handler_count() {
let mut bus = EventBus::new();
bus.subscribe(EventKind::TargetLoaded, Box::new(|_| {}));
bus.subscribe(EventKind::TargetUnloaded, Box::new(|_| {}));
assert_eq!(bus.handler_count(), 2);
}
#[test]
fn test_event_count_matches_published() {
let mut bus = EventBus::new();
bus.publish(Event {
kind: EventKind::ExportStarted,
payload: "null".to_string(),
timestamp_ms: ts(),
});
bus.publish(Event {
kind: EventKind::ExportFinished,
payload: "null".to_string(),
timestamp_ms: ts(),
});
assert_eq!(bus.event_count(), 2);
}
#[test]
fn test_multiple_handlers_same_kind() {
let counter = Arc::new(Mutex::new(0u32));
let c1 = Arc::clone(&counter);
let c2 = Arc::clone(&counter);
let mut bus = EventBus::new();
bus.subscribe(
EventKind::Error,
Box::new(move |_| *c1.lock().expect("should succeed") += 1),
);
bus.subscribe(
EventKind::Error,
Box::new(move |_| *c2.lock().expect("should succeed") += 1),
);
bus.publish(Event {
kind: EventKind::Error,
payload: "null".to_string(),
timestamp_ms: ts(),
});
assert_eq!(*counter.lock().expect("should succeed"), 2);
}
#[test]
fn test_custom_variant_matching() {
let counter = Arc::new(Mutex::new(0u32));
let c = Arc::clone(&counter);
let mut bus = EventBus::new();
bus.subscribe(
EventKind::Custom("my_event".to_string()),
Box::new(move |_| *c.lock().expect("should succeed") += 1),
);
bus.publish(Event {
kind: EventKind::Custom("my_event".to_string()),
payload: "null".to_string(),
timestamp_ms: ts(),
});
bus.publish(Event {
kind: EventKind::Custom("other".to_string()),
payload: "null".to_string(),
timestamp_ms: ts(),
});
assert_eq!(*counter.lock().expect("should succeed"), 1);
}
#[test]
fn test_make_param_changed_event_kind() {
let ev = make_param_changed_event("height", 1.75);
assert_eq!(ev.kind, EventKind::ParamChanged);
assert!(ev.payload.contains("height"));
}
#[test]
fn test_make_export_event_kind() {
let ev = make_export_event("/tmp/out.glb", "glb");
assert_eq!(ev.kind, EventKind::ExportStarted);
assert!(ev.payload.contains("glb"));
}
#[test]
fn test_make_error_event_kind() {
let ev = make_error_event("something went wrong");
assert_eq!(ev.kind, EventKind::Error);
assert!(ev.payload.contains("went wrong"));
}
#[test]
fn test_new_bus_empty() {
let bus = EventBus::new();
assert_eq!(bus.handler_count(), 0);
assert_eq!(bus.event_count(), 0);
}
#[test]
fn test_plugin_registered_event() {
let mut bus = EventBus::new();
let hit = Arc::new(Mutex::new(false));
let h = Arc::clone(&hit);
bus.subscribe(
EventKind::PluginRegistered,
Box::new(move |_| *h.lock().expect("should succeed") = true),
);
bus.publish(Event {
kind: EventKind::PluginRegistered,
payload: r#"{"name":"my-plugin"}"#.to_string(),
timestamp_ms: ts(),
});
assert!(*hit.lock().expect("should succeed"));
}
#[test]
fn test_history_payload_preserved() {
let mut bus = EventBus::new();
bus.publish(Event {
kind: EventKind::ParamChanged,
payload: r#"{"x":42}"#.to_string(),
timestamp_ms: 100,
});
assert_eq!(bus.history()[0].payload, r#"{"x":42}"#);
assert_eq!(bus.history()[0].timestamp_ms, 100);
}
}