use ahash::AHashMap as StdHashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum EventType {
RateLimitTriggered {
key: String,
rule_id: String,
decision: String,
},
BanApplied {
target: String,
reason: String,
duration: u64,
},
BanExpired {
target: String,
},
CircuitStateChanged {
from: String,
to: String,
},
QuotaAlert {
user_id: String,
resource: String,
usage_percent: f64,
},
}
impl EventType {
pub fn name(&self) -> &'static str {
match self {
EventType::RateLimitTriggered { .. } => "rate_limit_triggered",
EventType::BanApplied { .. } => "ban_applied",
EventType::BanExpired { .. } => "ban_expired",
EventType::CircuitStateChanged { .. } => "circuit_state_changed",
EventType::QuotaAlert { .. } => "quota_alert",
}
}
pub fn severity(&self) -> u8 {
match self {
EventType::RateLimitTriggered { decision, .. } => match decision.as_str() {
"Deny" => 6,
"Fallback" => 4,
_ => 2,
},
EventType::BanApplied { .. } => 8,
EventType::BanExpired { .. } => 1,
EventType::CircuitStateChanged { to, .. } => match to.as_str() {
"Open" => 9,
"HalfOpen" => 5,
"Closed" => 1,
_ => 3,
},
EventType::QuotaAlert { usage_percent, .. } => {
if *usage_percent >= 90.0 {
7
} else if *usage_percent >= 75.0 {
5
} else {
3
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub event_type: EventType,
pub timestamp: DateTime<Utc>,
#[serde(default)]
pub metadata: StdHashMap<String, serde_json::Value>,
}
impl Event {
pub fn new(event_type: EventType) -> Self {
Self {
event_type,
timestamp: Utc::now(),
metadata: StdHashMap::new(),
}
}
pub fn with_metadata(
event_type: EventType,
metadata: StdHashMap<String, serde_json::Value>,
) -> Self {
Self {
event_type,
timestamp: Utc::now(),
metadata,
}
}
pub fn add_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
self.metadata.insert(key.to_string(), value);
self
}
pub fn name(&self) -> &'static str {
self.event_type.name()
}
pub fn severity(&self) -> u8 {
self.event_type.severity()
}
}
#[async_trait::async_trait]
pub trait EventHandler: Send + Sync {
async fn handle(&self, event: &Event) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
fn name(&self) -> &str;
fn accepts(&self, _event_type: &EventType) -> bool {
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventConfig {
pub enabled: bool,
pub channel_capacity: usize,
pub webhook_enabled: bool,
pub webhook_urls: Vec<String>,
pub event_logging_enabled: bool,
}
impl Default for EventConfig {
fn default() -> Self {
Self {
enabled: true,
channel_capacity: 1024,
webhook_enabled: false,
webhook_urls: Vec::new(),
event_logging_enabled: true,
}
}
}
impl EventConfig {
pub fn enabled() -> Self {
Self {
enabled: true,
..Default::default()
}
}
pub fn with_channel_capacity(mut self, capacity: usize) -> Self {
self.channel_capacity = capacity;
self
}
pub fn with_webhook_urls(mut self, urls: Vec<String>) -> Self {
self.webhook_enabled = !urls.is_empty();
self.webhook_urls = urls;
self
}
pub fn without_logging(mut self) -> Self {
self.event_logging_enabled = false;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_type_name() {
let rate_limit = EventType::RateLimitTriggered {
key: "192.168.1.1".to_string(),
rule_id: "rule_1".to_string(),
decision: "Deny".to_string(),
};
assert_eq!(rate_limit.name(), "rate_limit_triggered");
let ban_applied = EventType::BanApplied {
target: "192.168.1.1".to_string(),
reason: "Excessive requests".to_string(),
duration: 3600,
};
assert_eq!(ban_applied.name(), "ban_applied");
}
#[test]
fn test_event_type_severity() {
let deny = EventType::RateLimitTriggered {
key: "key".to_string(),
rule_id: "rule".to_string(),
decision: "Deny".to_string(),
};
assert_eq!(deny.severity(), 6);
let circuit_open = EventType::CircuitStateChanged {
from: "Closed".to_string(),
to: "Open".to_string(),
};
assert_eq!(circuit_open.severity(), 9);
let quota_high = EventType::QuotaAlert {
user_id: "user1".to_string(),
resource: "api_calls".to_string(),
usage_percent: 95.0,
};
assert_eq!(quota_high.severity(), 7);
}
#[test]
fn test_event_type_name_all_variants() {
assert_eq!(
EventType::RateLimitTriggered {
key: "k".to_string(),
rule_id: "r".to_string(),
decision: "Deny".to_string(),
}
.name(),
"rate_limit_triggered"
);
assert_eq!(
EventType::BanApplied {
target: "t".to_string(),
reason: "r".to_string(),
duration: 1,
}
.name(),
"ban_applied"
);
assert_eq!(
EventType::BanExpired {
target: "t".to_string(),
}
.name(),
"ban_expired"
);
assert_eq!(
EventType::CircuitStateChanged {
from: "Closed".to_string(),
to: "Open".to_string(),
}
.name(),
"circuit_state_changed"
);
assert_eq!(
EventType::QuotaAlert {
user_id: "u".to_string(),
resource: "r".to_string(),
usage_percent: 50.0,
}
.name(),
"quota_alert"
);
}
#[test]
fn test_event_type_severity_all_branches() {
assert_eq!(
EventType::RateLimitTriggered {
key: "k".to_string(),
rule_id: "r".to_string(),
decision: "Deny".to_string(),
}
.severity(),
6
);
assert_eq!(
EventType::RateLimitTriggered {
key: "k".to_string(),
rule_id: "r".to_string(),
decision: "Fallback".to_string(),
}
.severity(),
4
);
assert_eq!(
EventType::RateLimitTriggered {
key: "k".to_string(),
rule_id: "r".to_string(),
decision: "Allow".to_string(),
}
.severity(),
2
);
assert_eq!(
EventType::BanApplied {
target: "t".to_string(),
reason: "r".to_string(),
duration: 60,
}
.severity(),
8
);
assert_eq!(
EventType::BanExpired {
target: "t".to_string(),
}
.severity(),
1
);
assert_eq!(
EventType::CircuitStateChanged {
from: "Closed".to_string(),
to: "Open".to_string(),
}
.severity(),
9
);
assert_eq!(
EventType::CircuitStateChanged {
from: "Open".to_string(),
to: "HalfOpen".to_string(),
}
.severity(),
5
);
assert_eq!(
EventType::CircuitStateChanged {
from: "HalfOpen".to_string(),
to: "Closed".to_string(),
}
.severity(),
1
);
assert_eq!(
EventType::CircuitStateChanged {
from: "X".to_string(),
to: "Unknown".to_string(),
}
.severity(),
3
);
assert_eq!(
EventType::QuotaAlert {
user_id: "u".to_string(),
resource: "r".to_string(),
usage_percent: 95.0,
}
.severity(),
7
);
assert_eq!(
EventType::QuotaAlert {
user_id: "u".to_string(),
resource: "r".to_string(),
usage_percent: 80.0,
}
.severity(),
5
);
assert_eq!(
EventType::QuotaAlert {
user_id: "u".to_string(),
resource: "r".to_string(),
usage_percent: 50.0,
}
.severity(),
3
);
}
#[test]
fn test_event_creation() {
let event_type = EventType::BanApplied {
target: "user123".to_string(),
reason: "Test".to_string(),
duration: 60,
};
let event = Event::new(event_type.clone());
assert!(event.timestamp <= Utc::now());
assert!(event.metadata.is_empty());
assert_eq!(event.name(), "ban_applied");
}
#[test]
fn test_event_with_metadata() {
let event_type = EventType::RateLimitTriggered {
key: "key".to_string(),
rule_id: "rule".to_string(),
decision: "Deny".to_string(),
};
let mut metadata = StdHashMap::new();
metadata.insert("request_id".to_string(), serde_json::json!("req_123"));
let event = Event::with_metadata(event_type, metadata.clone());
assert_eq!(event.metadata.len(), 1);
assert_eq!(
event.metadata.get("request_id"),
Some(&serde_json::json!("req_123"))
);
}
#[test]
fn test_event_add_metadata() {
let event_type = EventType::BanExpired {
target: "user123".to_string(),
};
let event = Event::new(event_type)
.add_metadata("cleanup_reason", serde_json::json!("auto_cleanup"));
assert_eq!(event.metadata.len(), 1);
}
#[test]
fn test_event_config_default() {
let config = EventConfig::default();
assert!(config.enabled);
assert_eq!(config.channel_capacity, 1024);
assert!(!config.webhook_enabled);
assert!(config.webhook_urls.is_empty());
assert!(config.event_logging_enabled);
}
#[test]
fn test_event_config_builder() {
let config = EventConfig::enabled()
.with_channel_capacity(2048)
.with_webhook_urls(vec!["http://example.com/hook".to_string()])
.without_logging();
assert!(config.enabled);
assert_eq!(config.channel_capacity, 2048);
assert!(config.webhook_enabled);
assert_eq!(config.webhook_urls.len(), 1);
assert!(!config.event_logging_enabled);
}
#[test]
fn test_event_serialization() {
let event_type = EventType::CircuitStateChanged {
from: "Closed".to_string(),
to: "Open".to_string(),
};
let event = Event::new(event_type);
let json = serde_json::to_string(&event).unwrap();
let deserialized: Event = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.name(), event.name());
}
#[test]
fn test_event_handler_accepts_default() {
struct DummyHandler;
#[async_trait::async_trait]
impl EventHandler for DummyHandler {
async fn handle(
&self,
_event: &Event,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(())
}
fn name(&self) -> &str {
"dummy"
}
}
let handler = DummyHandler;
let event_type = EventType::RateLimitTriggered {
key: "k".to_string(),
rule_id: "r".to_string(),
decision: "Deny".to_string(),
};
assert!(handler.accepts(&event_type));
let ban_event = EventType::BanApplied {
target: "t".to_string(),
reason: "r".to_string(),
duration: 60,
};
assert!(handler.accepts(&ban_event));
}
}