use crate::error::CacheResult;
use crate::traits::CacheStore;
use serde::{Serialize, de::DeserializeOwned};
use std::time::Duration;
pub async fn get<S: CacheStore, T: DeserializeOwned>(
store: &S,
key: &str,
) -> CacheResult<Option<T>> {
if let Some(json) = store.get_json(key).await? {
let value: T = serde_json::from_str(&json)
.map_err(|e| crate::error::CacheError::Deserialization(e.to_string()))?;
Ok(Some(value))
} else {
Ok(None)
}
}
pub async fn set<S: CacheStore, T: Serialize>(
store: &S,
key: &str,
value: &T,
ttl: Option<Duration>,
) -> CacheResult<()> {
let json = serde_json::to_string(value)
.map_err(|e| crate::error::CacheError::Serialization(e.to_string()))?;
store.set_json(key, json, ttl).await
}
pub async fn set_forever<S: CacheStore, T: Serialize>(
store: &S,
key: &str,
value: &T,
) -> CacheResult<()> {
let json = serde_json::to_string(value)
.map_err(|e| crate::error::CacheError::Serialization(e.to_string()))?;
store.set_json_forever(key, json).await
}
pub async fn remember<S: CacheStore, T, F, Fut>(
store: &S,
key: &str,
ttl: Duration,
factory: F,
) -> CacheResult<T>
where
T: Serialize + DeserializeOwned,
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = CacheResult<T>>,
{
if let Some(value) = get(store, key).await? {
return Ok(value);
}
let value = factory().await?;
set(store, key, &value, Some(ttl)).await?;
Ok(value)
}
pub async fn remember_forever<S: CacheStore, T, F, Fut>(
store: &S,
key: &str,
factory: F,
) -> CacheResult<T>
where
T: Serialize + DeserializeOwned,
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = CacheResult<T>>,
{
if let Some(value) = get(store, key).await? {
return Ok(value);
}
let value = factory().await?;
set_forever(store, key, &value).await?;
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
type WrittenEntries = Arc<RwLock<HashMap<String, (String, Option<Duration>)>>>;
struct DefaultTtlCache {
default_ttl: Option<Duration>,
data: WrittenEntries,
}
impl DefaultTtlCache {
fn new(default_ttl: Duration) -> Self {
Self {
default_ttl: Some(default_ttl),
data: Arc::new(RwLock::new(HashMap::new())),
}
}
async fn stored_ttl(&self, key: &str) -> Option<Duration> {
self.data.read().await.get(key).and_then(|(_, ttl)| *ttl)
}
}
#[async_trait]
impl CacheStore for DefaultTtlCache {
async fn get_json(&self, key: &str) -> CacheResult<Option<String>> {
Ok(self.data.read().await.get(key).map(|(v, _)| v.clone()))
}
async fn set_json(
&self,
key: &str,
value: String,
ttl: Option<Duration>,
) -> CacheResult<()> {
let effective = ttl.or(self.default_ttl);
self.data
.write()
.await
.insert(key.to_string(), (value, effective));
Ok(())
}
async fn set_json_forever(&self, key: &str, value: String) -> CacheResult<()> {
self.data
.write()
.await
.insert(key.to_string(), (value, None));
Ok(())
}
async fn delete(&self, key: &str) -> CacheResult<()> {
self.data.write().await.remove(key);
Ok(())
}
async fn exists(&self, key: &str) -> CacheResult<bool> {
Ok(self.data.read().await.contains_key(key))
}
async fn clear(&self) -> CacheResult<()> {
self.data.write().await.clear();
Ok(())
}
async fn ttl(&self, key: &str) -> CacheResult<Option<Duration>> {
Ok(self.stored_ttl(key).await)
}
async fn expire(&self, _key: &str, _ttl: Duration) -> CacheResult<()> {
Ok(())
}
async fn increment(&self, _key: &str, _delta: i64) -> CacheResult<i64> {
Ok(0)
}
async fn decrement(&self, _key: &str, _delta: i64) -> CacheResult<i64> {
Ok(0)
}
}
#[tokio::test]
async fn test_remember_forever_bypasses_default_ttl() {
let store = DefaultTtlCache::new(Duration::from_secs(300));
let value: i64 = remember_forever(&store, "k", || async { Ok(7) })
.await
.unwrap();
assert_eq!(value, 7);
assert_eq!(
store.stored_ttl("k").await,
None,
"remember_forever must store without expiry, not with the default TTL"
);
}
#[tokio::test]
async fn test_set_with_none_ttl_still_uses_default_ttl() {
let store = DefaultTtlCache::new(Duration::from_secs(300));
set(&store, "k", &7_i64, None).await.unwrap();
assert_eq!(
store.stored_ttl("k").await,
Some(Duration::from_secs(300)),
"an unspecified TTL must keep falling back to default_ttl"
);
set_forever(&store, "k2", &7_i64).await.unwrap();
assert_eq!(store.stored_ttl("k2").await, None);
}
#[tokio::test]
async fn test_remember_forever_returns_cached_value() {
let store = DefaultTtlCache::new(Duration::from_secs(300));
set_forever(&store, "k", &1_i64).await.unwrap();
let value: i64 = remember_forever(&store, "k", || async {
panic!("factory must not run on a cache hit")
})
.await
.unwrap();
assert_eq!(value, 1);
}
}