use std::hash::{BuildHasher, Hash};
use std::marker::PhantomData;
use std::time::Duration;
use foldhash::fast::RandomState;
use crate::tier::InMemoryCache;
#[derive(Debug)]
pub struct InMemoryCacheBuilder<K, V, H = RandomState> {
pub(crate) max_capacity: Option<u64>,
pub(crate) initial_capacity: Option<usize>,
pub(crate) time_to_live: Option<Duration>,
pub(crate) time_to_idle: Option<Duration>,
pub(crate) name: Option<&'static str>,
pub(crate) hasher: H,
_phantom: PhantomData<(K, V)>,
}
impl<K, V> Default for InMemoryCacheBuilder<K, V> {
fn default() -> Self {
Self::new()
}
}
impl<K, V> InMemoryCacheBuilder<K, V> {
#[must_use]
pub fn new() -> Self {
Self {
max_capacity: None,
initial_capacity: None,
time_to_live: None,
time_to_idle: None,
name: None,
hasher: RandomState::default(),
_phantom: PhantomData,
}
}
}
impl<K, V, H> InMemoryCacheBuilder<K, V, H> {
#[must_use]
pub fn max_capacity(mut self, capacity: u64) -> Self {
self.max_capacity = Some(capacity);
self
}
#[must_use]
pub fn initial_capacity(mut self, capacity: usize) -> Self {
self.initial_capacity = Some(capacity);
self
}
#[must_use]
pub fn time_to_live(mut self, duration: Duration) -> Self {
self.time_to_live = Some(duration);
self
}
#[must_use]
pub fn time_to_idle(mut self, duration: Duration) -> Self {
self.time_to_idle = Some(duration);
self
}
#[must_use]
pub fn name(mut self, name: &'static str) -> Self {
self.name = Some(name);
self
}
#[must_use]
pub fn with_hasher<H2>(self, hasher: H2) -> InMemoryCacheBuilder<K, V, H2> {
InMemoryCacheBuilder {
max_capacity: self.max_capacity,
initial_capacity: self.initial_capacity,
time_to_live: self.time_to_live,
time_to_idle: self.time_to_idle,
name: self.name,
hasher,
_phantom: PhantomData,
}
}
pub fn build(self) -> Result<InMemoryCache<K, V, H>, ValidationError>
where
K: Hash + Eq + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
H: BuildHasher + Clone + Send + Sync + 'static,
{
self.validate()?;
Ok(InMemoryCache::from_builder(&self))
}
fn validate(&self) -> Result<(), ValidationError> {
ValidationError::invalid_capacity(self.max_capacity, self.initial_capacity).map_or(Ok(()), Err)?;
ValidationError::invalid_time_to(self.time_to_live, self.time_to_idle).map_or(Ok(()), Err)?;
Ok(())
}
pub(crate) fn build_unchecked(self) -> InMemoryCache<K, V, H>
where
K: Hash + Eq + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
H: BuildHasher + Clone + Send + Sync + 'static,
{
InMemoryCache::from_builder(&self)
}
}
#[ohno::error]
#[display("invalid cache configuration: {reason}")]
pub struct ValidationError {
reason: String,
}
impl ValidationError {
fn invalid_capacity(max_capacity: Option<u64>, initial_capacity: Option<usize>) -> Option<Self> {
let max = max_capacity?;
let init = initial_capacity?;
(init as u64 > max).then(|| Self::new(format!("initial_capacity ({init}) exceeds max_capacity ({max})")))
}
fn invalid_time_to(time_to_live: Option<Duration>, time_to_idle: Option<Duration>) -> Option<Self> {
let time_to_idle = time_to_idle?;
let time_to_live = time_to_live?;
(time_to_idle > time_to_live)
.then(|| Self::new(format!("time to idle ({time_to_idle:?}) exceeds time to live ({time_to_live:?}).")))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn max_capacity_stores_value() {
let builder = InMemoryCacheBuilder::<String, i32>::new().max_capacity(100);
assert_eq!(builder.max_capacity, Some(100));
}
#[test]
fn initial_capacity_stores_value() {
let builder = InMemoryCacheBuilder::<String, i32>::new().initial_capacity(50);
assert_eq!(builder.initial_capacity, Some(50));
}
#[test]
fn time_to_live_stores_value() {
let builder = InMemoryCacheBuilder::<String, i32>::new().time_to_live(Duration::from_secs(300));
assert_eq!(builder.time_to_live, Some(Duration::from_secs(300)));
}
#[test]
fn time_to_idle_stores_value() {
let builder = InMemoryCacheBuilder::<String, i32>::new().time_to_idle(Duration::from_secs(60));
assert_eq!(builder.time_to_idle, Some(Duration::from_secs(60)));
}
#[test]
fn name_stores_value() {
let builder = InMemoryCacheBuilder::<String, i32>::new().name("test");
assert_eq!(builder.name, Some("test"));
}
#[test]
fn build_max_capacity_lt_initial_capacity_returns_validation_error() {
let result = InMemoryCacheBuilder::<String, i32>::new()
.max_capacity(100)
.initial_capacity(101)
.build();
ohno::assert_error_message!(
result.unwrap_err(),
"invalid cache configuration: initial_capacity (101) exceeds max_capacity (100)"
);
}
#[cfg_attr(miri, ignore)] #[test]
fn build_max_capacity_eq_initial_capacity_succeeds() {
let result = InMemoryCacheBuilder::<String, i32>::new()
.max_capacity(100)
.initial_capacity(100)
.build();
result.unwrap();
}
#[test]
fn build_ttl_less_than_tti_returns_validation_error() {
let result = InMemoryCacheBuilder::<String, i32>::new()
.time_to_live(Duration::from_secs(60))
.time_to_idle(Duration::from_secs(120))
.build();
ohno::assert_error_message!(
result.unwrap_err(),
"invalid cache configuration: time to idle (120s) exceeds time to live (60s)."
);
}
#[cfg_attr(miri, ignore)] #[test]
fn build_ttl_eq_tti_succeeds() {
let result = InMemoryCacheBuilder::<String, i32>::new()
.time_to_live(Duration::from_secs(60))
.time_to_idle(Duration::from_secs(60))
.build();
result.unwrap();
}
}