use crate::backend::interface::CacheBackend;
use crate::backend::memory::moka::MokaMemoryBackend;
use crate::cache::Cache;
use crate::error::{CacheError, Result};
use crate::traits::CacheKey;
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;
pub struct CacheBuilder<K, V> {
backends: Vec<Arc<dyn CacheBackend>>,
ttl: Option<Duration>,
tti: Option<Duration>,
capacity: Option<u64>,
sync_mode: bool,
_phantom: PhantomData<(K, V)>,
}
impl<K, V> Default for CacheBuilder<K, V> {
fn default() -> Self {
Self {
backends: Vec::new(),
ttl: None,
tti: None,
capacity: None,
sync_mode: false,
_phantom: PhantomData,
}
}
}
impl<K, V> CacheBuilder<K, V>
where
K: CacheKey,
V: serde::Serialize + for<'de> serde::Deserialize<'de>,
{
pub fn backend_arc(mut self, backend: Arc<dyn CacheBackend>) -> Self {
self.backends.push(backend);
self
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn tti(mut self, tti: Duration) -> Self {
self.tti = Some(tti);
self
}
pub fn capacity(mut self, capacity: u64) -> Self {
self.capacity = Some(capacity);
self
}
pub fn sync_mode(mut self, enabled: bool) -> Self {
self.sync_mode = enabled;
self
}
pub async fn build(self) -> Result<Cache<K, V>> {
if self.sync_mode && !self.backends.is_empty() {
return Err(CacheError::NotSupported(
"sync_mode(true) cannot be combined with backend_arc(); \
Arc<dyn CacheBackend> cannot be upcast to Arc<dyn SyncCacheBackend> \
in stable Rust (no trait_upcasting). Use the default Moka backend \
with sync_mode, or construct the Cache manually via \
Cache::new_with_backend + set_sync_backend."
.to_string(),
));
}
if self.backends.is_empty() {
let capacity = self.capacity.unwrap_or(10000);
let mut builder = MokaMemoryBackend::builder().capacity(capacity);
if let Some(ttl) = self.ttl {
builder = builder.ttl(ttl);
}
if let Some(tti) = self.tti {
builder = builder.time_to_idle(tti);
}
let moka = Arc::new(builder.build());
let mut cache = Cache::new_with_backend(moka.clone());
if self.sync_mode {
cache.set_sync_backend(moka);
}
return Ok(cache);
}
let backend = self.backends[0].clone();
Ok(Cache::new_with_backend(backend))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_default() {
let builder: CacheBuilder<String, String> = CacheBuilder::default();
assert!(builder.backends.is_empty());
assert!(builder.ttl.is_none());
}
#[tokio::test]
async fn test_builder_empty() {
let cache: Cache<String, i32> = Cache::builder().build().await.unwrap();
cache.set(&"key".to_string(), &42).await.unwrap();
assert_eq!(cache.get(&"key".to_string()).await.unwrap().unwrap(), 42);
}
#[tokio::test]
async fn test_builder_single_backend() {
let backend = MokaMemoryBackend::builder().capacity(100).build();
let cache: Cache<String, i32> = Cache::builder().backend_arc(Arc::new(backend)).build().await.unwrap();
cache.set(&"key".to_string(), &42).await.unwrap();
assert_eq!(cache.get(&"key".to_string()).await.unwrap().unwrap(), 42);
}
#[test]
fn test_builder_ttl() {
let builder: CacheBuilder<String, String> = CacheBuilder::default().ttl(Duration::from_secs(60));
assert_eq!(builder.ttl, Some(Duration::from_secs(60)));
}
#[test]
fn test_builder_ttl_zero() {
let builder: CacheBuilder<String, String> = CacheBuilder::default().ttl(Duration::from_secs(0));
assert_eq!(builder.ttl, Some(Duration::from_secs(0)));
}
#[test]
fn test_builder_ttl_chained() {
let builder: CacheBuilder<String, String> = CacheBuilder::default().ttl(Duration::from_secs(30)).capacity(100);
assert_eq!(builder.ttl, Some(Duration::from_secs(30)));
assert_eq!(builder.capacity, Some(100));
}
#[test]
fn test_builder_tti() {
let builder: CacheBuilder<String, String> = CacheBuilder::default().tti(Duration::from_secs(120));
assert_eq!(builder.tti, Some(Duration::from_secs(120)));
}
#[test]
fn test_builder_tti_zero() {
let builder: CacheBuilder<String, String> = CacheBuilder::default().tti(Duration::from_secs(0));
assert_eq!(builder.tti, Some(Duration::from_secs(0)));
}
#[test]
fn test_builder_tti_chained() {
let builder: CacheBuilder<String, String> = CacheBuilder::default()
.tti(Duration::from_secs(45))
.ttl(Duration::from_secs(300));
assert_eq!(builder.tti, Some(Duration::from_secs(45)));
assert_eq!(builder.ttl, Some(Duration::from_secs(300)));
}
#[test]
fn test_builder_capacity() {
let builder: CacheBuilder<String, String> = CacheBuilder::default().capacity(10000);
assert_eq!(builder.capacity, Some(10000));
}
#[test]
fn test_builder_capacity_zero() {
let builder: CacheBuilder<String, String> = CacheBuilder::default().capacity(0);
assert_eq!(builder.capacity, Some(0));
}
#[test]
fn test_builder_capacity_chained() {
let builder: CacheBuilder<String, String> = CacheBuilder::default()
.capacity(500)
.ttl(Duration::from_secs(60))
.tti(Duration::from_secs(30));
assert_eq!(builder.capacity, Some(500));
assert_eq!(builder.ttl, Some(Duration::from_secs(60)));
assert_eq!(builder.tti, Some(Duration::from_secs(30)));
}
#[test]
fn test_builder_backend_arc() {
let backend = MokaMemoryBackend::builder().capacity(100).build();
let builder: CacheBuilder<String, String> = CacheBuilder::default().backend_arc(Arc::new(backend));
assert_eq!(builder.backends.len(), 1);
}
#[test]
fn test_builder_backend_arc_multiple() {
let backend1 = MokaMemoryBackend::builder().capacity(100).build();
let backend2 = MokaMemoryBackend::builder().capacity(200).build();
let builder: CacheBuilder<String, String> = CacheBuilder::default()
.backend_arc(Arc::new(backend1))
.backend_arc(Arc::new(backend2));
assert_eq!(builder.backends.len(), 2);
}
#[tokio::test]
async fn test_builder_build_with_ttl() {
let cache: Cache<String, i32> = Cache::builder().ttl(Duration::from_secs(60)).build().await.unwrap();
cache.set(&"key".to_string(), &42).await.unwrap();
assert_eq!(cache.get(&"key".to_string()).await.unwrap().unwrap(), 42);
}
#[tokio::test]
async fn test_builder_build_with_tti() {
let cache: Cache<String, i32> = Cache::builder().tti(Duration::from_secs(60)).build().await.unwrap();
cache.set(&"key".to_string(), &42).await.unwrap();
assert_eq!(cache.get(&"key".to_string()).await.unwrap().unwrap(), 42);
}
#[tokio::test]
async fn test_builder_build_with_capacity() {
let cache: Cache<String, i32> = Cache::builder().capacity(100).build().await.unwrap();
cache.set(&"key".to_string(), &42).await.unwrap();
assert_eq!(cache.get(&"key".to_string()).await.unwrap().unwrap(), 42);
}
#[tokio::test]
async fn test_builder_build_with_ttl_and_tti() {
let cache: Cache<String, i32> = Cache::builder()
.ttl(Duration::from_secs(60))
.tti(Duration::from_secs(30))
.capacity(100)
.build()
.await
.unwrap();
cache.set(&"key".to_string(), &42).await.unwrap();
assert_eq!(cache.get(&"key".to_string()).await.unwrap().unwrap(), 42);
}
#[test]
fn test_builder_default_capacity_none() {
let builder: CacheBuilder<String, String> = CacheBuilder::default();
assert!(builder.capacity.is_none());
}
#[test]
fn test_builder_default_tti_none() {
let builder: CacheBuilder<String, String> = CacheBuilder::default();
assert!(builder.tti.is_none());
}
#[test]
fn test_builder_default_backends_empty() {
let builder: CacheBuilder<String, String> = CacheBuilder::default();
assert!(builder.backends.is_empty());
}
#[test]
fn test_builder_full_chain() {
let backend = MokaMemoryBackend::builder().capacity(100).build();
let builder: CacheBuilder<String, String> = CacheBuilder::default()
.ttl(Duration::from_secs(60))
.tti(Duration::from_secs(30))
.capacity(1000)
.backend_arc(Arc::new(backend));
assert_eq!(builder.ttl, Some(Duration::from_secs(60)));
assert_eq!(builder.tti, Some(Duration::from_secs(30)));
assert_eq!(builder.capacity, Some(1000));
assert_eq!(builder.backends.len(), 1);
}
#[test]
fn test_builder_sync_mode_true() {
let builder: CacheBuilder<String, String> = CacheBuilder::default().sync_mode(true);
assert!(builder.sync_mode, "sync_mode(true) should set field to true");
}
#[test]
fn test_builder_sync_mode_false_explicit() {
let builder: CacheBuilder<String, String> = CacheBuilder::default().sync_mode(false);
assert!(!builder.sync_mode, "sync_mode(false) should set field to false");
}
#[test]
fn test_builder_default_sync_mode_false() {
let builder: CacheBuilder<String, String> = CacheBuilder::default();
assert!(!builder.sync_mode, "default sync_mode should be false");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_builder_sync_mode_true_enables_backend_sync() {
let cache: Cache<String, String> = Cache::builder().sync_mode(true).build().await.unwrap();
cache.set_sync(&"k".to_string(), &"v".to_string()).unwrap();
assert_eq!(cache.get_sync(&"k".to_string()).unwrap(), Some("v".to_string()));
}
#[tokio::test]
async fn test_builder_default_sync_mode_false_backend_sync_none() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
let result = cache.get_sync(&"k".to_string());
assert!(
matches!(result, Err(crate::error::CacheError::NotSupported(_))),
"expected Err(NotSupported) when sync_mode is false, got {:?}",
result
);
}
#[tokio::test]
async fn test_builder_sync_mode_with_unsupported_backend_returns_err() {
let backend = MokaMemoryBackend::builder().capacity(100).build();
let result: crate::error::Result<Cache<String, String>> = Cache::builder()
.backend_arc(Arc::new(backend))
.sync_mode(true)
.build()
.await;
assert!(result.is_err(), "sync_mode(true) + backend_arc() should return Err");
match result {
Err(crate::error::CacheError::NotSupported(msg)) => {
assert!(
msg.contains("sync_mode") || msg.contains("backend_arc"),
"error message should explain the sync_mode+backend_arc limitation, got: {}",
msg
);
}
Err(e) => panic!("expected NotSupported, got {:?}", e),
Ok(_) => panic!("expected error, got Ok"),
}
}
}