use crate::error::CacheError;
use async_trait::async_trait;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CacheEventType {
Hit,
Miss,
Set,
Delete,
Expire,
Clear,
Get,
BatchStart,
BatchEnd,
Error,
Connect,
Disconnect,
Custom(String),
}
impl fmt::Display for CacheEventType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CacheEventType::Hit => write!(f, "hit"),
CacheEventType::Miss => write!(f, "miss"),
CacheEventType::Set => write!(f, "set"),
CacheEventType::Delete => write!(f, "delete"),
CacheEventType::Expire => write!(f, "expire"),
CacheEventType::Clear => write!(f, "clear"),
CacheEventType::Get => write!(f, "get"),
CacheEventType::BatchStart => write!(f, "batch_start"),
CacheEventType::BatchEnd => write!(f, "batch_end"),
CacheEventType::Error => write!(f, "error"),
CacheEventType::Connect => write!(f, "connect"),
CacheEventType::Disconnect => write!(f, "disconnect"),
CacheEventType::Custom(s) => write!(f, "custom:{}", s),
}
}
}
#[derive(Debug, Clone)]
pub struct CacheEvent {
pub event_type: CacheEventType,
pub key: Option<String>,
pub timestamp: u64,
pub latency_ms: Option<u64>,
pub error: Option<String>,
pub metadata: Vec<(String, String)>,
}
impl CacheEvent {
pub fn new(event_type: CacheEventType) -> Self {
Self {
event_type,
key: None,
timestamp: current_timestamp_ms(),
latency_ms: None,
error: None,
metadata: Vec::new(),
}
}
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn with_latency(mut self, latency_ms: u64) -> Self {
self.latency_ms = Some(latency_ms);
self
}
pub fn with_error(mut self, error: impl Into<String>) -> Self {
self.error = Some(error.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.push((key.into(), value.into()));
self
}
}
fn current_timestamp_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
#[async_trait]
pub trait EventPublisher: Send + Sync {
async fn publish(&self, event: CacheEvent) -> Result<(), CacheError>;
fn publish_hit(&self, _key: impl Into<String>, _latency_ms: u64) -> Result<(), CacheError> {
Ok(())
}
fn publish_miss(&self, _key: impl Into<String>, _latency_ms: u64) -> Result<(), CacheError> {
Ok(())
}
fn publish_set(&self, _key: impl Into<String>) -> Result<(), CacheError> {
Ok(())
}
fn publish_delete(&self, _key: impl Into<String>) -> Result<(), CacheError> {
Ok(())
}
fn publish_error(&self, _key: Option<String>, _error: impl Into<String>) -> Result<(), CacheError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_event_creation() {
let event = CacheEvent::new(CacheEventType::Hit).with_key("test_key");
assert_eq!(event.event_type, CacheEventType::Hit);
assert_eq!(event.key, Some("test_key".to_string()));
}
#[test]
fn test_cache_event_type_display() {
assert_eq!(CacheEventType::Hit.to_string(), "hit");
assert_eq!(CacheEventType::Miss.to_string(), "miss");
assert_eq!(CacheEventType::Custom("test".to_string()).to_string(), "custom:test");
}
#[test]
fn test_cache_event_with_value() {
let event = CacheEvent::new(CacheEventType::Set).with_key("user:1").with_latency(5);
assert_eq!(event.key, Some("user:1".to_string()));
assert_eq!(event.latency_ms, Some(5));
}
#[test]
fn test_cache_event_with_metadata() {
let event = CacheEvent::new(CacheEventType::Miss)
.with_key("missing_key")
.with_latency(5)
.with_metadata("test_node", "test_service");
assert_eq!(event.latency_ms, Some(5));
assert_eq!(event.metadata.len(), 1);
assert_eq!(event.metadata[0], ("test_node".to_string(), "test_service".to_string()));
}
#[test]
fn test_cache_event_error() {
let event = CacheEvent::new(CacheEventType::Error)
.with_key("error_key")
.with_error("Connection timeout");
assert_eq!(event.event_type, CacheEventType::Error);
assert_eq!(event.error, Some("Connection timeout".to_string()));
}
#[test]
fn test_cache_event_types_complete() {
assert_eq!(CacheEventType::Expire.to_string(), "expire");
assert_eq!(CacheEventType::Clear.to_string(), "clear");
assert_eq!(CacheEventType::Get.to_string(), "get");
assert_eq!(CacheEventType::BatchStart.to_string(), "batch_start");
assert_eq!(CacheEventType::BatchEnd.to_string(), "batch_end");
assert_eq!(CacheEventType::Error.to_string(), "error");
assert_eq!(CacheEventType::Connect.to_string(), "connect");
assert_eq!(CacheEventType::Disconnect.to_string(), "disconnect");
}
#[test]
fn test_cache_event_with_all_optional_fields() {
let event = CacheEvent::new(CacheEventType::Get)
.with_key("test_key")
.with_latency(100)
.with_metadata("node1", "service1")
.with_error("test error");
assert_eq!(event.event_type, CacheEventType::Get);
assert_eq!(event.key, Some("test_key".to_string()));
assert_eq!(event.latency_ms, Some(100));
assert_eq!(event.metadata.len(), 1); assert_eq!(event.error, Some("test error".to_string()));
}
#[test]
fn test_cache_event_clone() {
let event = CacheEvent::new(CacheEventType::Hit).with_key("key").with_latency(10);
let cloned = event.clone();
assert_eq!(event.event_type, cloned.event_type);
assert_eq!(event.key, cloned.key);
assert_eq!(event.latency_ms, cloned.latency_ms);
}
#[test]
fn test_cache_event_type_set_display() {
assert_eq!(CacheEventType::Set.to_string(), "set");
}
#[test]
fn test_cache_event_type_delete_display() {
assert_eq!(CacheEventType::Delete.to_string(), "delete");
}
struct NoopPublisher;
#[async_trait]
impl EventPublisher for NoopPublisher {
async fn publish(&self, _event: CacheEvent) -> Result<(), CacheError> {
Ok(())
}
}
#[tokio::test]
async fn test_event_publisher_publish_hit_default() {
let publisher = NoopPublisher;
let result = publisher.publish_hit("key1", 10);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_event_publisher_publish_miss_default() {
let publisher = NoopPublisher;
let result = publisher.publish_miss("key1", 10);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_event_publisher_publish_set_default() {
let publisher = NoopPublisher;
let result = publisher.publish_set("key1");
assert!(result.is_ok());
}
#[tokio::test]
async fn test_event_publisher_publish_delete_default() {
let publisher = NoopPublisher;
let result = publisher.publish_delete("key1");
assert!(result.is_ok());
}
#[tokio::test]
async fn test_event_publisher_publish_error_default() {
let publisher = NoopPublisher;
let result = publisher.publish_error(Some("key1".to_string()), "timeout");
assert!(result.is_ok());
}
#[tokio::test]
async fn test_event_publisher_publish_error_default_none_key() {
let publisher = NoopPublisher;
let result = publisher.publish_error(None, "connection failed");
assert!(result.is_ok());
}
#[tokio::test]
async fn test_event_publisher_publish() {
let publisher = NoopPublisher;
let event = CacheEvent::new(CacheEventType::Hit).with_key("key1");
let result = publisher.publish(event).await;
assert!(result.is_ok());
}
#[test]
fn test_cache_event_new_default_fields() {
let event = CacheEvent::new(CacheEventType::Set);
assert_eq!(event.event_type, CacheEventType::Set);
assert!(event.key.is_none());
assert!(event.latency_ms.is_none());
assert!(event.error.is_none());
assert!(event.metadata.is_empty());
assert!(event.timestamp > 0);
}
#[test]
fn test_cache_event_with_multiple_metadata() {
let event = CacheEvent::new(CacheEventType::Get)
.with_key("test_key")
.with_metadata("node", "node1")
.with_metadata("service", "service1")
.with_metadata("region", "us-east");
assert_eq!(event.metadata.len(), 3);
assert_eq!(event.metadata[0], ("node".to_string(), "node1".to_string()));
assert_eq!(event.metadata[1], ("service".to_string(), "service1".to_string()));
assert_eq!(event.metadata[2], ("region".to_string(), "us-east".to_string()));
}
#[test]
fn test_cache_event_type_equality() {
assert_eq!(CacheEventType::Hit, CacheEventType::Hit);
assert_ne!(CacheEventType::Hit, CacheEventType::Miss);
assert_eq!(
CacheEventType::Custom("test".to_string()),
CacheEventType::Custom("test".to_string())
);
assert_ne!(
CacheEventType::Custom("test1".to_string()),
CacheEventType::Custom("test2".to_string())
);
}
#[test]
fn test_cache_event_debug() {
let event = CacheEvent::new(CacheEventType::Hit).with_key("key");
let debug_str = format!("{:?}", event);
assert!(debug_str.contains("CacheEvent"));
assert!(debug_str.contains("Hit"));
}
}