use crate::events::CacheEvent;
use std::hash::Hash;
use std::sync::Arc;
use std::time::Duration;
use tower_resilience_core::{EventListeners, FnListener};
pub type KeyExtractor<Req, K> = Arc<dyn Fn(&Req) -> K + Send + Sync>;
pub struct CacheConfig<Req, K> {
pub(crate) max_size: usize,
pub(crate) ttl: Option<Duration>,
pub(crate) key_extractor: KeyExtractor<Req, K>,
pub(crate) event_listeners: EventListeners<CacheEvent>,
pub(crate) name: String,
}
impl<Req, K> CacheConfig<Req, K>
where
K: Hash + Eq + Clone + Send + 'static,
{
pub fn builder() -> CacheConfigBuilder<Req, K> {
CacheConfigBuilder::new()
}
pub fn layer(self) -> crate::CacheLayer<Req, K> {
crate::CacheLayer::new(self)
}
}
pub struct CacheConfigBuilder<Req, K> {
max_size: usize,
ttl: Option<Duration>,
key_extractor: Option<KeyExtractor<Req, K>>,
event_listeners: EventListeners<CacheEvent>,
name: String,
}
impl<Req, K> CacheConfigBuilder<Req, K>
where
K: Hash + Eq + Clone + Send + 'static,
{
pub fn new() -> Self {
Self {
max_size: 100,
ttl: None,
key_extractor: None,
event_listeners: EventListeners::new(),
name: String::from("<unnamed>"),
}
}
pub fn max_size(mut self, size: usize) -> Self {
self.max_size = size;
self
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn key_extractor<F>(mut self, f: F) -> Self
where
F: Fn(&Req) -> K + Send + Sync + 'static,
{
self.key_extractor = Some(Arc::new(f));
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn on_hit<F>(mut self, f: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.event_listeners.add(FnListener::new(move |event| {
if matches!(event, CacheEvent::Hit { .. }) {
f();
}
}));
self
}
pub fn on_miss<F>(mut self, f: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.event_listeners.add(FnListener::new(move |event| {
if matches!(event, CacheEvent::Miss { .. }) {
f();
}
}));
self
}
pub fn on_eviction<F>(mut self, f: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.event_listeners.add(FnListener::new(move |event| {
if matches!(event, CacheEvent::Eviction { .. }) {
f();
}
}));
self
}
pub fn build(self) -> CacheConfig<Req, K> {
let key_extractor = self
.key_extractor
.expect("key_extractor must be set before building");
CacheConfig {
max_size: self.max_size,
ttl: self.ttl,
key_extractor,
event_listeners: self.event_listeners,
name: self.name,
}
}
}
impl<Req, K> Default for CacheConfigBuilder<Req, K>
where
K: Hash + Eq + Clone + Send + 'static,
{
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Clone, Hash, Eq, PartialEq)]
struct TestRequest {
id: String,
}
#[test]
fn test_builder_defaults() {
let config = CacheConfig::<TestRequest, String>::builder()
.key_extractor(|req| req.id.clone())
.build();
assert_eq!(config.max_size, 100);
assert!(config.ttl.is_none());
assert_eq!(config.name, "<unnamed>");
}
#[test]
fn test_builder_custom_values() {
let config = CacheConfig::<TestRequest, String>::builder()
.max_size(500)
.ttl(Duration::from_secs(60))
.key_extractor(|req| req.id.clone())
.name("my-cache")
.build();
assert_eq!(config.max_size, 500);
assert_eq!(config.ttl, Some(Duration::from_secs(60)));
assert_eq!(config.name, "my-cache");
}
#[test]
fn test_event_listeners() {
let hit_count = Arc::new(AtomicUsize::new(0));
let miss_count = Arc::new(AtomicUsize::new(0));
let eviction_count = Arc::new(AtomicUsize::new(0));
let hc = Arc::clone(&hit_count);
let mc = Arc::clone(&miss_count);
let ec = Arc::clone(&eviction_count);
let config = CacheConfig::<TestRequest, String>::builder()
.key_extractor(|req| req.id.clone())
.on_hit(move || {
hc.fetch_add(1, Ordering::SeqCst);
})
.on_miss(move || {
mc.fetch_add(1, Ordering::SeqCst);
})
.on_eviction(move || {
ec.fetch_add(1, Ordering::SeqCst);
})
.build();
use std::time::Instant;
config.event_listeners.emit(&CacheEvent::Hit {
pattern_name: "test".to_string(),
timestamp: Instant::now(),
});
assert_eq!(hit_count.load(Ordering::SeqCst), 1);
config.event_listeners.emit(&CacheEvent::Miss {
pattern_name: "test".to_string(),
timestamp: Instant::now(),
});
assert_eq!(miss_count.load(Ordering::SeqCst), 1);
config.event_listeners.emit(&CacheEvent::Eviction {
pattern_name: "test".to_string(),
timestamp: Instant::now(),
});
assert_eq!(eviction_count.load(Ordering::SeqCst), 1);
}
#[test]
#[should_panic(expected = "key_extractor must be set")]
fn test_builder_panics_without_key_extractor() {
let _config = CacheConfig::<TestRequest, String>::builder().build();
}
}