use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SubscriptionId(u64);
impl SubscriptionId {
#[must_use]
pub const fn as_u64(self) -> u64 {
self.0
}
}
impl std::fmt::Display for SubscriptionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "sub-{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BusEvent {
pub topic: String,
pub source: String,
pub payload: serde_json::Value,
pub timestamp_ms: u64,
pub correlation_id: Option<String>,
pub metadata: HashMap<String, String>,
}
impl BusEvent {
#[must_use]
pub fn new(
topic: impl Into<String>,
source: impl Into<String>,
payload: serde_json::Value,
timestamp_ms: u64,
) -> Self {
Self {
topic: topic.into(),
source: source.into(),
payload,
timestamp_ms,
correlation_id: None,
metadata: HashMap::new(),
}
}
#[must_use]
pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
self.correlation_id = Some(id.into());
self
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone)]
pub struct EventFilter {
pub source_prefix: Option<String>,
pub metadata_match: HashMap<String, String>,
pub min_values: Vec<(String, f64)>,
}
impl Default for EventFilter {
fn default() -> Self {
Self {
source_prefix: None,
metadata_match: HashMap::new(),
min_values: Vec::new(),
}
}
}
impl EventFilter {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_source_prefix(mut self, prefix: impl Into<String>) -> Self {
self.source_prefix = Some(prefix.into());
self
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata_match.insert(key.into(), value.into());
self
}
#[must_use]
pub fn with_min_value(mut self, field: impl Into<String>, min: f64) -> Self {
self.min_values.push((field.into(), min));
self
}
#[must_use]
pub fn matches(&self, event: &BusEvent) -> bool {
if let Some(ref prefix) = self.source_prefix {
if !event.source.starts_with(prefix) {
return false;
}
}
for (key, expected) in &self.metadata_match {
match event.metadata.get(key) {
Some(actual) if actual == expected => {}
_ => return false,
}
}
for (field, min_val) in &self.min_values {
if let Some(val) = event.payload.get(field).and_then(|v| v.as_f64()) {
if val < *min_val {
return false;
}
} else {
return false; }
}
true
}
}
#[derive(Debug, Clone)]
struct Subscription {
id: SubscriptionId,
topic_pattern: String,
filter: EventFilter,
delivered: Vec<BusEvent>,
max_retained: usize,
active: bool,
}
impl Subscription {
fn topic_matches(&self, topic: &str) -> bool {
if self.topic_pattern == "*" {
return true;
}
if self.topic_pattern.ends_with(".*") {
let prefix = &self.topic_pattern[..self.topic_pattern.len() - 2];
return topic.starts_with(prefix);
}
self.topic_pattern == topic
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeadLetterEntry {
pub event: BusEvent,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct EventBus {
inner: Arc<Mutex<EventBusInner>>,
}
#[derive(Debug)]
struct EventBusInner {
subscriptions: Vec<Subscription>,
next_sub_id: u64,
event_history: Vec<BusEvent>,
dead_letters: Vec<DeadLetterEntry>,
max_history: usize,
max_dead_letters: usize,
total_published: u64,
total_delivered: u64,
}
#[derive(Debug, Clone)]
pub struct EventBusStats {
pub active_subscriptions: usize,
pub total_subscriptions: usize,
pub total_published: u64,
pub total_delivered: u64,
pub history_size: usize,
pub dead_letter_count: usize,
}
#[derive(Debug, Clone)]
pub struct EventBusConfig {
pub max_history: usize,
pub max_dead_letters: usize,
pub default_max_retained: usize,
}
impl Default for EventBusConfig {
fn default() -> Self {
Self {
max_history: 10_000,
max_dead_letters: 1_000,
default_max_retained: 100,
}
}
}
impl EventBus {
#[must_use]
pub fn new() -> Self {
Self::with_config(EventBusConfig::default())
}
#[must_use]
pub fn with_config(config: EventBusConfig) -> Self {
Self {
inner: Arc::new(Mutex::new(EventBusInner {
subscriptions: Vec::new(),
next_sub_id: 1,
event_history: Vec::new(),
dead_letters: Vec::new(),
max_history: config.max_history,
max_dead_letters: config.max_dead_letters,
total_published: 0,
total_delivered: 0,
})),
}
}
pub fn subscribe(
&self,
topic_pattern: impl Into<String>,
filter: EventFilter,
) -> SubscriptionId {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
let id = SubscriptionId(inner.next_sub_id);
inner.next_sub_id += 1;
inner.subscriptions.push(Subscription {
id,
topic_pattern: topic_pattern.into(),
filter,
delivered: Vec::new(),
max_retained: 100,
active: true,
});
id
}
pub fn subscribe_with_retention(
&self,
topic_pattern: impl Into<String>,
filter: EventFilter,
max_retained: usize,
) -> SubscriptionId {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
let id = SubscriptionId(inner.next_sub_id);
inner.next_sub_id += 1;
inner.subscriptions.push(Subscription {
id,
topic_pattern: topic_pattern.into(),
filter,
delivered: Vec::new(),
max_retained,
active: true,
});
id
}
pub fn unsubscribe(&self, sub_id: SubscriptionId) -> bool {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
if let Some(sub) = inner.subscriptions.iter_mut().find(|s| s.id == sub_id) {
sub.active = false;
true
} else {
false
}
}
pub fn publish(&self, event: BusEvent) -> usize {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.total_published += 1;
let mut delivery_count = 0usize;
for sub in inner.subscriptions.iter_mut() {
if !sub.active {
continue;
}
if !sub.topic_matches(&event.topic) {
continue;
}
if !sub.filter.matches(&event) {
continue;
}
sub.delivered.push(event.clone());
delivery_count += 1;
if sub.max_retained > 0 && sub.delivered.len() > sub.max_retained {
let excess = sub.delivered.len() - sub.max_retained;
sub.delivered.drain(..excess);
}
}
inner.total_delivered += delivery_count as u64;
if delivery_count == 0 {
inner.dead_letters.push(DeadLetterEntry {
event: event.clone(),
reason: "no matching subscribers".to_string(),
});
if inner.max_dead_letters > 0 && inner.dead_letters.len() > inner.max_dead_letters {
inner.dead_letters.remove(0);
}
}
inner.event_history.push(event);
if inner.max_history > 0 && inner.event_history.len() > inner.max_history {
inner.event_history.remove(0);
}
delivery_count
}
#[must_use]
pub fn get_events(&self, sub_id: SubscriptionId) -> Vec<BusEvent> {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner
.subscriptions
.iter()
.find(|s| s.id == sub_id)
.map_or_else(Vec::new, |s| s.delivered.clone())
}
pub fn drain_events(&self, sub_id: SubscriptionId) -> Vec<BusEvent> {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner
.subscriptions
.iter_mut()
.find(|s| s.id == sub_id)
.map_or_else(Vec::new, |s| std::mem::take(&mut s.delivered))
}
#[must_use]
pub fn dead_letters(&self) -> Vec<DeadLetterEntry> {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.dead_letters.clone()
}
#[must_use]
pub fn event_history(&self) -> Vec<BusEvent> {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.event_history.clone()
}
#[must_use]
pub fn events_by_topic(&self, topic: &str) -> Vec<BusEvent> {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner
.event_history
.iter()
.filter(|e| e.topic == topic)
.cloned()
.collect()
}
#[must_use]
pub fn stats(&self) -> EventBusStats {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
EventBusStats {
active_subscriptions: inner.subscriptions.iter().filter(|s| s.active).count(),
total_subscriptions: inner.subscriptions.len(),
total_published: inner.total_published,
total_delivered: inner.total_delivered,
history_size: inner.event_history.len(),
dead_letter_count: inner.dead_letters.len(),
}
}
pub fn clear(&self) {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.event_history.clear();
inner.dead_letters.clear();
for sub in &mut inner.subscriptions {
sub.delivered.clear();
}
}
pub fn replay_to(&self, sub_id: SubscriptionId) -> usize {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
let sub_idx = match inner.subscriptions.iter().position(|s| s.id == sub_id) {
Some(idx) => idx,
None => return 0,
};
let history = inner.event_history.clone();
let mut count = 0;
for event in &history {
let sub = &inner.subscriptions[sub_idx];
if sub.active && sub.topic_matches(&event.topic) && sub.filter.matches(event) {
inner.subscriptions[sub_idx].delivered.push(event.clone());
count += 1;
}
}
count
}
}
impl Default for EventBus {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_event(topic: &str, source: &str) -> BusEvent {
BusEvent::new(topic, source, serde_json::json!({}), 1000)
}
#[test]
fn test_event_creation() {
let event = make_event("task.done", "task-1");
assert_eq!(event.topic, "task.done");
assert_eq!(event.source, "task-1");
assert_eq!(event.timestamp_ms, 1000);
assert!(event.correlation_id.is_none());
}
#[test]
fn test_event_with_correlation() {
let event = make_event("task.done", "task-1").with_correlation_id("corr-123");
assert_eq!(event.correlation_id, Some("corr-123".to_string()));
}
#[test]
fn test_event_with_metadata() {
let event = make_event("task.done", "task-1").with_metadata("workflow", "wf-1");
assert_eq!(event.metadata.get("workflow"), Some(&"wf-1".to_string()));
}
#[test]
fn test_empty_filter_matches_all() {
let filter = EventFilter::new();
let event = make_event("any.topic", "any-source");
assert!(filter.matches(&event));
}
#[test]
fn test_source_prefix_filter() {
let filter = EventFilter::new().with_source_prefix("task-");
assert!(filter.matches(&make_event("x", "task-1")));
assert!(!filter.matches(&make_event("x", "workflow-1")));
}
#[test]
fn test_metadata_filter() {
let filter = EventFilter::new().with_metadata("env", "production");
let event = make_event("x", "y").with_metadata("env", "production");
assert!(filter.matches(&event));
let event2 = make_event("x", "y").with_metadata("env", "staging");
assert!(!filter.matches(&event2));
}
#[test]
fn test_min_value_filter() {
let filter = EventFilter::new().with_min_value("quality", 90.0);
let event = BusEvent::new("x", "y", serde_json::json!({"quality": 95.0}), 0);
assert!(filter.matches(&event));
let event2 = BusEvent::new("x", "y", serde_json::json!({"quality": 80.0}), 0);
assert!(!filter.matches(&event2));
}
#[test]
fn test_min_value_missing_field() {
let filter = EventFilter::new().with_min_value("quality", 90.0);
let event = BusEvent::new("x", "y", serde_json::json!({"other": 95.0}), 0);
assert!(!filter.matches(&event));
}
#[test]
fn test_bus_subscribe_and_publish() {
let bus = EventBus::new();
let sub = bus.subscribe("task.done", EventFilter::new());
let delivered = bus.publish(make_event("task.done", "task-1"));
assert_eq!(delivered, 1);
let events = bus.get_events(sub);
assert_eq!(events.len(), 1);
assert_eq!(events[0].source, "task-1");
}
#[test]
fn test_bus_no_match() {
let bus = EventBus::new();
bus.subscribe("task.done", EventFilter::new());
let delivered = bus.publish(make_event("workflow.started", "wf-1"));
assert_eq!(delivered, 0);
}
#[test]
fn test_bus_wildcard_topic() {
let bus = EventBus::new();
let sub = bus.subscribe("task.*", EventFilter::new());
bus.publish(make_event("task.done", "t1"));
bus.publish(make_event("task.failed", "t2"));
bus.publish(make_event("workflow.started", "wf1"));
let events = bus.get_events(sub);
assert_eq!(events.len(), 2);
}
#[test]
fn test_bus_catch_all() {
let bus = EventBus::new();
let sub = bus.subscribe("*", EventFilter::new());
bus.publish(make_event("task.done", "t1"));
bus.publish(make_event("workflow.started", "wf1"));
let events = bus.get_events(sub);
assert_eq!(events.len(), 2);
}
#[test]
fn test_bus_multiple_subscribers() {
let bus = EventBus::new();
let sub1 = bus.subscribe("task.done", EventFilter::new());
let sub2 = bus.subscribe("task.done", EventFilter::new());
let sub3 = bus.subscribe("task.failed", EventFilter::new());
let delivered = bus.publish(make_event("task.done", "t1"));
assert_eq!(delivered, 2);
assert_eq!(bus.get_events(sub1).len(), 1);
assert_eq!(bus.get_events(sub2).len(), 1);
assert_eq!(bus.get_events(sub3).len(), 0);
}
#[test]
fn test_bus_unsubscribe() {
let bus = EventBus::new();
let sub = bus.subscribe("task.done", EventFilter::new());
bus.publish(make_event("task.done", "t1"));
assert_eq!(bus.get_events(sub).len(), 1);
assert!(bus.unsubscribe(sub));
bus.publish(make_event("task.done", "t2"));
assert_eq!(bus.get_events(sub).len(), 1);
}
#[test]
fn test_bus_unsubscribe_nonexistent() {
let bus = EventBus::new();
assert!(!bus.unsubscribe(SubscriptionId(999)));
}
#[test]
fn test_bus_drain_events() {
let bus = EventBus::new();
let sub = bus.subscribe("task.done", EventFilter::new());
bus.publish(make_event("task.done", "t1"));
bus.publish(make_event("task.done", "t2"));
let drained = bus.drain_events(sub);
assert_eq!(drained.len(), 2);
assert_eq!(bus.get_events(sub).len(), 0);
}
#[test]
fn test_dead_letters() {
let bus = EventBus::new();
bus.publish(make_event("orphan.event", "src"));
let dead = bus.dead_letters();
assert_eq!(dead.len(), 1);
assert_eq!(dead[0].event.topic, "orphan.event");
}
#[test]
fn test_dead_letters_not_for_delivered() {
let bus = EventBus::new();
bus.subscribe("task.done", EventFilter::new());
bus.publish(make_event("task.done", "t1"));
assert!(bus.dead_letters().is_empty());
}
#[test]
fn test_event_history() {
let bus = EventBus::new();
bus.publish(make_event("a", "s1"));
bus.publish(make_event("b", "s2"));
let history = bus.event_history();
assert_eq!(history.len(), 2);
}
#[test]
fn test_events_by_topic() {
let bus = EventBus::new();
bus.publish(make_event("task.done", "t1"));
bus.publish(make_event("task.failed", "t2"));
bus.publish(make_event("task.done", "t3"));
let done_events = bus.events_by_topic("task.done");
assert_eq!(done_events.len(), 2);
}
#[test]
fn test_stats() {
let bus = EventBus::new();
let _sub1 = bus.subscribe("task.done", EventFilter::new());
let sub2 = bus.subscribe("task.done", EventFilter::new());
bus.unsubscribe(sub2);
bus.publish(make_event("task.done", "t1"));
bus.publish(make_event("orphan", "s1"));
let stats = bus.stats();
assert_eq!(stats.active_subscriptions, 1);
assert_eq!(stats.total_subscriptions, 2);
assert_eq!(stats.total_published, 2);
assert_eq!(stats.total_delivered, 1);
assert_eq!(stats.history_size, 2);
assert_eq!(stats.dead_letter_count, 1);
}
#[test]
fn test_retention_limit() {
let bus = EventBus::new();
let sub = bus.subscribe_with_retention("task.done", EventFilter::new(), 3);
for i in 0..5 {
bus.publish(make_event("task.done", &format!("t{i}")));
}
let events = bus.get_events(sub);
assert_eq!(events.len(), 3);
assert_eq!(events[0].source, "t2");
assert_eq!(events[2].source, "t4");
}
#[test]
fn test_clear() {
let bus = EventBus::new();
let sub = bus.subscribe("task.done", EventFilter::new());
bus.publish(make_event("task.done", "t1"));
bus.publish(make_event("orphan", "s1"));
bus.clear();
assert!(bus.event_history().is_empty());
assert!(bus.dead_letters().is_empty());
assert!(bus.get_events(sub).is_empty());
}
#[test]
fn test_replay_to_subscription() {
let bus = EventBus::new();
bus.publish(make_event("task.done", "t1"));
bus.publish(make_event("task.done", "t2"));
bus.publish(make_event("workflow.started", "wf1"));
let sub = bus.subscribe("task.done", EventFilter::new());
assert_eq!(bus.get_events(sub).len(), 0);
let replayed = bus.replay_to(sub);
assert_eq!(replayed, 2);
assert_eq!(bus.get_events(sub).len(), 2);
}
#[test]
fn test_topic_and_filter_combined() {
let bus = EventBus::new();
let filter = EventFilter::new().with_source_prefix("encoder-");
let sub = bus.subscribe("task.done", filter);
bus.publish(make_event("task.done", "encoder-1"));
bus.publish(make_event("task.done", "decoder-1"));
let events = bus.get_events(sub);
assert_eq!(events.len(), 1);
assert_eq!(events[0].source, "encoder-1");
}
#[test]
fn test_subscription_id_display() {
let id = SubscriptionId(42);
assert_eq!(id.to_string(), "sub-42");
assert_eq!(id.as_u64(), 42);
}
#[test]
fn test_bus_is_clone_and_send() {
let bus = EventBus::new();
let bus2 = bus.clone();
let sub = bus.subscribe("test", EventFilter::new());
bus2.publish(make_event("test", "src"));
assert_eq!(bus.get_events(sub).len(), 1);
}
}