use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Clone)]
struct CacheEntry<V> {
value: V,
inserted_at: Instant,
ttl: Duration,
}
impl<V> CacheEntry<V> {
fn new(value: V, ttl: Duration) -> Self {
Self {
value,
inserted_at: Instant::now(),
ttl,
}
}
fn is_expired(&self) -> bool {
self.inserted_at.elapsed() > self.ttl
}
}
pub struct ResponseCache {
storage: Arc<RwLock<HashMap<String, CacheEntry<serde_json::Value>>>>,
default_ttl: Duration,
}
impl ResponseCache {
pub fn new(ttl_seconds: u64) -> Self {
Self {
storage: Arc::new(RwLock::new(HashMap::new())),
default_ttl: Duration::from_secs(ttl_seconds),
}
}
pub async fn get(&self, key: &str) -> Option<serde_json::Value> {
let storage = self.storage.read().await;
if let Some(entry) = storage.get(key) {
if !entry.is_expired() {
return Some(entry.value.clone());
}
}
None
}
pub async fn put(&self, key: String, value: serde_json::Value) {
let mut storage = self.storage.write().await;
storage.insert(key, CacheEntry::new(value, self.default_ttl));
}
pub async fn put_with_ttl(&self, key: String, value: serde_json::Value, ttl: Duration) {
let mut storage = self.storage.write().await;
storage.insert(key, CacheEntry::new(value, ttl));
}
pub async fn remove(&self, key: &str) -> Option<serde_json::Value> {
let mut storage = self.storage.write().await;
storage.remove(key).map(|entry| entry.value)
}
pub async fn cleanup_expired(&self) -> usize {
let mut storage = self.storage.write().await;
let expired_keys: Vec<String> = storage
.iter()
.filter(|(_, entry)| entry.is_expired())
.map(|(key, _)| key.clone())
.collect();
let count = expired_keys.len();
for key in expired_keys {
storage.remove(&key);
}
count
}
pub async fn clear(&self) {
let mut storage = self.storage.write().await;
storage.clear();
}
pub async fn size(&self) -> usize {
let storage = self.storage.read().await;
storage.len()
}
}
pub fn generate_cache_key(method: &str, path: &str, body: Option<&serde_json::Value>) -> String {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
method.hash(&mut hasher);
path.hash(&mut hasher);
if let Some(body) = body {
if let Ok(json_str) = serde_json::to_string(body) {
json_str.hash(&mut hasher);
}
}
format!("{}:{}:{:x}", method, path, hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn test_cache_get_put() {
let cache = ResponseCache::new(60);
let value = json!({"message": "test"});
cache.put("test_key".to_string(), value.clone()).await;
let retrieved = cache.get("test_key").await;
assert_eq!(retrieved, Some(value));
}
#[tokio::test]
async fn test_cache_expiration() {
let cache = ResponseCache::new(1);
let value = json!({"message": "test"});
cache.put("test_key".to_string(), value.clone()).await;
assert!(cache.get("test_key").await.is_some());
tokio::time::sleep(Duration::from_secs(2)).await;
assert!(cache.get("test_key").await.is_none());
}
#[tokio::test]
async fn test_cache_cleanup() {
let cache = ResponseCache::new(1);
cache.put("key1".to_string(), json!("value1")).await;
cache.put("key2".to_string(), json!("value2")).await;
tokio::time::sleep(Duration::from_secs(2)).await;
let cleaned = cache.cleanup_expired().await;
assert_eq!(cleaned, 2);
assert_eq!(cache.size().await, 0);
}
#[test]
fn test_cache_key_generation() {
let key1 = generate_cache_key("GET", "/api/users", None);
let key2 = generate_cache_key("GET", "/api/users", None);
let key3 = generate_cache_key("POST", "/api/users", None);
assert_eq!(key1, key2);
assert_ne!(key1, key3);
}
#[test]
fn test_cache_key_with_body() {
let body1 = json!({"name": "Alice"});
let body2 = json!({"name": "Bob"});
let key1 = generate_cache_key("POST", "/api/users", Some(&body1));
let key2 = generate_cache_key("POST", "/api/users", Some(&body2));
assert_ne!(key1, key2);
}
}