pub mod store;
use std::future::Future;
use std::sync::Arc;
use cot::config::CacheStoreTypeConfig;
use derive_more::with_trait::Debug;
use serde::Serialize;
use serde::de::DeserializeOwned;
use thiserror::Error;
use crate::cache::store::memory::Memory;
#[cfg(feature = "redis")]
use crate::cache::store::redis::Redis;
use crate::cache::store::{BoxCacheStore, CacheStore};
use crate::config::{CacheConfig, Timeout};
use crate::error::error_impl::impl_into_cot_error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CacheError {
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
#[error(transparent)]
Store(#[from] store::CacheStoreError),
}
impl_into_cot_error!(CacheError, INTERNAL_SERVER_ERROR);
pub type CacheResult<T> = Result<T, CacheError>;
#[derive(Debug, Clone)]
pub struct Cache {
inner: Arc<CacheImpl>,
}
#[derive(Debug)]
struct CacheImpl {
#[debug("..")]
store: Box<dyn BoxCacheStore>,
prefix: Option<String>,
expiry: Timeout,
}
impl Cache {
pub fn new(store: impl CacheStore, prefix: Option<String>, expiry: Timeout) -> Self {
let store: Box<dyn BoxCacheStore> = Box::new(store);
Self {
inner: Arc::new(CacheImpl {
store,
prefix,
expiry,
}),
}
}
fn format_key<K: AsRef<str>>(&self, key: K) -> String {
let k = key.as_ref();
if let Some(pref) = &self.inner.prefix {
return format!("{pref}:{k}");
}
k.to_string()
}
pub async fn get<K, V>(&self, key: K) -> CacheResult<Option<V>>
where
K: AsRef<str>,
V: DeserializeOwned,
{
let k = self.format_key(key.as_ref());
let result = self
.inner
.store
.get(&k)
.await?
.map(serde_json::from_value)
.transpose()?;
Ok(result)
}
pub async fn insert<K, V>(&self, key: K, value: V) -> CacheResult<()>
where
K: Into<String>,
V: Serialize,
{
let k = self.format_key(key.into());
self.inner
.store
.insert(k, serde_json::to_value(value)?, self.inner.expiry)
.await?;
Ok(())
}
pub async fn insert_expiring<K, V>(&self, key: K, value: V, expiry: Timeout) -> CacheResult<()>
where
K: Into<String>,
V: Serialize,
{
let k = self.format_key(key.into());
self.inner
.store
.insert(k, serde_json::to_value(value)?, expiry)
.await?;
Ok(())
}
pub async fn remove<K: AsRef<str>>(&self, key: K) -> CacheResult<()> {
let k = self.format_key(key.as_ref());
self.inner.store.remove(&k).await?;
Ok(())
}
pub async fn clear(&self) -> CacheResult<()> {
self.inner.store.clear().await?;
Ok(())
}
pub async fn approx_size(&self) -> CacheResult<usize> {
let result = self.inner.store.approx_size().await?;
Ok(result)
}
pub async fn contains_key<K: AsRef<str>>(&self, key: K) -> CacheResult<bool> {
let k = self.format_key(key.as_ref());
let result = self.inner.store.contains_key(&k).await?;
Ok(result)
}
pub async fn insert_with<F, Fut, K, V>(&self, key: K, f: F) -> CacheResult<()>
where
F: FnOnce() -> Fut + Send,
Fut: Future<Output = CacheResult<V>> + Send,
K: Into<String>,
V: DeserializeOwned + Serialize,
{
let computed_value = f().await?;
self.insert(key.into(), computed_value).await?;
Ok(())
}
pub async fn get_or_insert_with<F, Fut, K, V>(&self, key: K, f: F) -> CacheResult<V>
where
K: Into<String>,
F: FnOnce() -> Fut + Send,
Fut: Future<Output = CacheResult<V>> + Send,
V: DeserializeOwned + Serialize,
{
let key = key.into();
if let Some(value) = self.get(&key).await? {
return Ok(value);
}
let computed_value = f().await?;
let value = serde_json::to_value(&computed_value)?;
self.insert(key, serde_json::to_value(&value)?).await?;
Ok(computed_value)
}
pub async fn get_or_insert_expiring_with<F, Fut, K, V>(
&self,
key: K,
f: F,
expiry: Timeout,
) -> CacheResult<V>
where
K: Into<String>,
F: FnOnce() -> Fut + Send,
Fut: Future<Output = CacheResult<V>> + Send,
V: DeserializeOwned + Serialize,
{
let key = key.into();
let value = self.get(&key).await?;
if let Some(value) = value {
return Ok(value);
}
let computed_value = f().await?;
let value = serde_json::to_value(&computed_value)?;
self.insert_expiring(key, serde_json::to_value(&value)?, expiry)
.await?;
Ok(computed_value)
}
#[expect(clippy::unused_async)]
pub async fn from_config(config: &CacheConfig) -> CacheResult<Self> {
let store_cfg = &config.store;
let this = {
match store_cfg.store_type {
CacheStoreTypeConfig::Memory => {
let mem_store = Memory::new();
Self::new(mem_store, config.prefix.clone(), config.timeout)
}
#[cfg(feature = "redis")]
CacheStoreTypeConfig::Redis { ref url, pool_size } => {
let redis_store = Redis::new(url, pool_size)?;
Self::new(redis_store, config.prefix.clone(), config.timeout)
}
_ => {
unimplemented!();
}
}
};
Ok(this)
}
}
#[cfg(test)]
mod tests {
use std::fmt::Debug;
use std::time::Duration;
use cot::config::CacheUrl;
use serde::{Deserialize, Serialize};
use super::*;
use crate::cache::store::memory::Memory;
use crate::config::Timeout;
use crate::test::TestCache;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct User {
id: u32,
name: String,
email: String,
}
#[cot_macros::cachetest]
async fn test_cache_basic_operations(test_cache: &mut TestCache) {
let cache = test_cache.cache();
cache
.insert("user:1", "John Doe".to_string())
.await
.unwrap();
let user: Option<String> = cache.get("user:1").await.unwrap();
assert_eq!(user, Some("John Doe".to_string()));
cache.remove("user:1").await.unwrap();
let user: Option<String> = cache.get("user:1").await.unwrap();
assert_eq!(user, None);
}
#[cot::test]
async fn test_cache_with_prefix() {
let store = Memory::new();
let cache = Cache::new(
store,
Some("myapp".to_string()),
Timeout::After(Duration::from_secs(60)),
);
cache.insert("user:1", "John Doe").await.unwrap();
let user: Option<String> = cache.get("user:1").await.unwrap();
assert_eq!(user, Some("John Doe".to_string()));
}
#[cot_macros::cachetest]
async fn test_cache_complex_objects(test_cache: &mut TestCache) {
let cache = test_cache.cache();
let user = User {
id: 1,
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
};
cache.insert("user:1", &user).await.unwrap();
let cached_user: Option<User> = cache.get("user:1").await.unwrap();
assert_eq!(cached_user, Some(user));
}
#[cot_macros::cachetest]
async fn test_cache_insert_expiring(test_cache: &mut TestCache) {
let cache = test_cache.cache();
cache
.insert_expiring(
"temp:data",
"temporary",
Timeout::After(Duration::from_secs(300)),
)
.await
.unwrap();
let value: Option<String> = cache.get("temp:data").await.unwrap();
assert_eq!(value, Some("temporary".to_string()));
}
#[cot_macros::cachetest]
async fn test_cache_get_or_insert_with(test_cache: &mut TestCache) {
let cache = test_cache.cache();
let mut call_count = 0;
let value1: String = cache
.get_or_insert_with("expensive", || async {
call_count += 1;
Ok("computed".to_string())
})
.await
.unwrap();
let value2: String = cache
.get_or_insert_with("expensive", || async {
call_count += 1;
Ok("different".to_string())
})
.await
.unwrap();
assert_eq!(value1, value2);
assert_eq!(call_count, 1);
}
#[cot_macros::cachetest]
async fn test_cache_get_or_insert_with_expiring(test_cache: &mut TestCache) {
let cache = test_cache.cache();
let mut call_count = 0;
let value1: String = cache
.get_or_insert_expiring_with(
"temp:data",
|| async {
call_count += 1;
Ok("temporary".to_string())
},
Timeout::After(Duration::from_secs(300)),
)
.await
.unwrap();
let value2: String = cache
.get_or_insert_expiring_with(
"temp:data",
|| async {
call_count += 1;
Ok("different".to_string())
},
Timeout::After(Duration::from_secs(300)),
)
.await
.unwrap();
assert_eq!(value1, value2);
assert_eq!(call_count, 1);
}
#[cot_macros::cachetest]
async fn test_cache_statistics(test_cache: &mut TestCache) {
let cache = test_cache.cache();
assert_eq!(cache.approx_size().await.unwrap(), 0);
cache.insert("key1", "value1").await.unwrap();
cache.insert("key2", "value2").await.unwrap();
assert_eq!(cache.approx_size().await.unwrap(), 2);
cache.clear().await.unwrap();
assert_eq!(cache.approx_size().await.unwrap(), 0);
}
#[cot_macros::cachetest]
async fn test_cache_contains_key(test_cache: &mut TestCache) {
let cache = test_cache.cache();
assert!(!cache.contains_key("nonexistent").await.unwrap());
cache.insert("existing", "value").await.unwrap();
assert!(cache.contains_key("existing").await.unwrap());
}
#[cfg(feature = "redis")]
#[cot::test]
async fn test_cache_from_config_redis() {
use crate::config::{CacheConfig, CacheStoreConfig, CacheStoreTypeConfig};
let url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost".to_string());
let url = CacheUrl::from(url);
let config = CacheConfig::builder()
.store(
CacheStoreConfig::builder()
.store_type(CacheStoreTypeConfig::Redis { url, pool_size: 5 })
.build(),
)
.prefix("test_redis")
.timeout(Timeout::After(Duration::from_secs(60)))
.build();
let result = Cache::from_config(&config).await;
assert!(result.is_ok());
}
}