use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub ttl: Duration,
pub max_entries: usize,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
ttl: Duration::from_secs(300),
max_entries: 500,
}
}
}
impl CacheConfig {
#[must_use]
pub fn short_lived() -> Self {
Self {
ttl: Duration::from_secs(60),
max_entries: 100,
}
}
#[must_use]
pub fn long_lived() -> Self {
Self {
ttl: Duration::from_secs(3600),
max_entries: 1000,
}
}
}
#[derive(Debug, Clone)]
struct CacheEntry {
data: Vec<u8>,
expires_at: Instant,
last_accessed: Instant,
}
impl CacheEntry {
fn is_expired(&self) -> bool {
Instant::now() >= self.expires_at
}
}
#[derive(Clone)]
pub struct Cache<K = String>
where
K: Eq + Hash + Clone + Send + Sync,
{
entries: Arc<RwLock<HashMap<K, CacheEntry>>>,
config: CacheConfig,
}
impl<K> Cache<K>
where
K: Eq + Hash + Clone + Send + Sync,
{
#[must_use]
pub fn new() -> Self {
Self::with_config(CacheConfig::default())
}
#[must_use]
pub fn with_config(config: CacheConfig) -> Self {
Self {
entries: Arc::new(RwLock::new(HashMap::new())),
config,
}
}
pub async fn insert<V>(&self, key: K, value: &V)
where
V: Serialize,
{
let data = match serde_json::to_vec(value) {
Ok(d) => d,
Err(_) => return,
};
let entry = CacheEntry {
data,
expires_at: Instant::now() + self.config.ttl,
last_accessed: Instant::now(),
};
let mut entries = self.entries.write().await;
if entries.len() >= self.config.max_entries && !entries.contains_key(&key) {
self.evict_lru(&mut entries);
}
entries.insert(key, entry);
}
pub async fn get<V>(&self, key: &K) -> Option<V>
where
V: for<'de> Deserialize<'de>,
{
{
let entries = self.entries.read().await;
if let Some(entry) = entries.get(key) {
if entry.is_expired() {
drop(entries);
self.remove(key).await;
return None;
}
if let Ok(value) = serde_json::from_slice(&entry.data) {
drop(entries);
let mut entries = self.entries.write().await;
if let Some(entry) = entries.get_mut(key) {
entry.last_accessed = Instant::now();
}
return Some(value);
}
}
}
None
}
pub async fn remove(&self, key: &K) {
let mut entries = self.entries.write().await;
entries.remove(key);
}
pub async fn clear(&self) {
let mut entries = self.entries.write().await;
entries.clear();
}
pub async fn len(&self) -> usize {
self.entries.read().await.len()
}
pub async fn is_empty(&self) -> bool {
self.entries.read().await.is_empty()
}
pub async fn cleanup_expired(&self) {
let mut entries = self.entries.write().await;
entries.retain(|_, entry| !entry.is_expired());
}
pub async fn contains_key(&self, key: &K) -> bool {
let entries = self.entries.read().await;
if let Some(entry) = entries.get(key) {
!entry.is_expired()
} else {
false
}
}
fn evict_lru(&self, entries: &mut HashMap<K, CacheEntry>) {
if let Some((key_to_remove, _)) = entries
.iter()
.min_by_key(|(_, entry)| entry.last_accessed)
.map(|(k, e)| (k.clone(), e.last_accessed))
{
entries.remove(&key_to_remove);
}
}
}
impl<K> Default for Cache<K>
where
K: Eq + Hash + Clone + Send + Sync,
{
fn default() -> Self {
Self::new()
}
}
impl<K> std::fmt::Debug for Cache<K>
where
K: Eq + Hash + Clone + Send + Sync,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Cache")
.field("config", &self.config)
.finish()
}
}
#[must_use]
pub fn cache_key(client: &str, tags: &[String], limit: u32, page: u32) -> String {
let mut tags_sorted = tags.to_vec();
tags_sorted.sort();
format!(
"{}:{}:limit={}:page={}",
client,
tags_sorted.join(","),
limit,
page
)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_insert_get() {
let cache = Cache::<String>::new();
let value = vec![1, 2, 3];
cache.insert("test".to_string(), &value).await;
let retrieved: Option<Vec<i32>> = cache.get(&"test".to_string()).await;
assert_eq!(retrieved, Some(value));
}
#[tokio::test]
async fn test_expiration() {
let cache = Cache::<String>::with_config(CacheConfig {
ttl: Duration::from_millis(50),
max_entries: 100,
});
cache.insert("test".to_string(), &"value").await;
assert!(cache.contains_key(&"test".to_string()).await);
tokio::time::sleep(Duration::from_millis(100)).await;
let result: Option<String> = cache.get(&"test".to_string()).await;
assert!(result.is_none());
}
#[tokio::test]
async fn test_lru_eviction() {
let cache = Cache::<String>::with_config(CacheConfig {
ttl: Duration::from_secs(60),
max_entries: 2,
});
cache.insert("a".to_string(), &1).await;
cache.insert("b".to_string(), &2).await;
let _: Option<i32> = cache.get(&"a".to_string()).await;
cache.insert("c".to_string(), &3).await;
assert!(cache.contains_key(&"a".to_string()).await);
assert!(!cache.contains_key(&"b".to_string()).await);
assert!(cache.contains_key(&"c".to_string()).await);
}
#[test]
fn test_cache_key() {
let key = cache_key(
"danbooru",
&["blue_eyes".to_string(), "cat_ears".to_string()],
10,
0,
);
assert!(key.starts_with("danbooru:"));
assert!(key.contains("limit=10"));
assert!(key.contains("page=0"));
}
}