use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::hash::Hash;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheEntry<K, V, M = ()>
where
K: Clone + Hash + Eq,
V: Clone,
M: Clone,
{
pub key: K,
pub value: V,
pub metadata: M,
pub timestamp: DateTime<Utc>,
pub expiry: Option<DateTime<Utc>>,
pub access_count: u64,
pub last_accessed: DateTime<Utc>,
}
impl<K, V, M> CacheEntry<K, V, M>
where
K: Clone + Hash + Eq,
V: Clone,
M: Clone + Default,
{
pub fn new(key: K, value: V) -> Self {
let now = Utc::now();
Self {
key,
value,
metadata: M::default(),
timestamp: now,
expiry: None,
access_count: 0,
last_accessed: now,
}
}
}
impl<K, V, M> CacheEntry<K, V, M>
where
K: Clone + Hash + Eq,
V: Clone,
M: Clone,
{
pub fn with_metadata(key: K, value: V, metadata: M) -> Self {
let now = Utc::now();
Self {
key,
value,
metadata,
timestamp: now,
expiry: None,
access_count: 0,
last_accessed: now,
}
}
pub fn with_ttl(mut self, ttl: chrono::Duration) -> Self {
self.expiry = Some(self.timestamp + ttl);
self
}
pub fn is_expired(&self) -> bool {
if let Some(expiry) = self.expiry {
Utc::now() > expiry
} else {
false
}
}
pub fn record_access(&mut self) {
self.access_count += 1;
self.last_accessed = Utc::now();
}
pub fn age(&self) -> chrono::Duration {
Utc::now() - self.timestamp
}
}
pub trait EntryMetadata:
Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync + 'static
{
fn execution_time_ms(&self) -> Option<u64> {
None
}
fn size_bytes(&self) -> Option<u64> {
None
}
fn category(&self) -> Option<&str> {
None
}
}
impl EntryMetadata for () {}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BasicMetadata {
pub execution_time_ms: Option<u64>,
pub size_bytes: Option<u64>,
pub category: Option<String>,
pub tags: Vec<String>,
}
impl EntryMetadata for BasicMetadata {
fn execution_time_ms(&self) -> Option<u64> {
self.execution_time_ms
}
fn size_bytes(&self) -> Option<u64> {
self.size_bytes
}
fn category(&self) -> Option<&str> {
self.category.as_deref()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EntryStatistics {
pub total_count: usize,
pub total_size_bytes: u64,
pub avg_execution_time_ms: f64,
pub avg_age_seconds: f64,
pub expired_count: usize,
pub total_access_count: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_entry_creation() {
#[allow(clippy::type_complexity)]
let entry: CacheEntry<String, String, ()> =
CacheEntry::new("key1".to_string(), "value1".to_string());
assert_eq!(entry.key, "key1");
assert_eq!(entry.value, "value1");
assert_eq!(entry.access_count, 0);
assert!(!entry.is_expired());
}
#[test]
fn test_cache_entry_ttl() {
#[allow(clippy::type_complexity)]
let entry: CacheEntry<String, String, ()> =
CacheEntry::new("key1".to_string(), "value1".to_string())
.with_ttl(chrono::Duration::seconds(60));
assert!(entry.expiry.is_some());
assert!(!entry.is_expired());
}
#[test]
fn test_cache_entry_metadata() {
let metadata = BasicMetadata {
execution_time_ms: Some(100),
size_bytes: Some(1024),
category: Some("test".to_string()),
tags: vec!["tag1".to_string()],
};
let entry = CacheEntry::with_metadata("key1".to_string(), "value1".to_string(), metadata);
assert_eq!(entry.metadata.execution_time_ms(), Some(100));
assert_eq!(entry.metadata.size_bytes(), Some(1024));
assert_eq!(entry.metadata.category(), Some("test"));
}
#[test]
#[allow(clippy::type_complexity)]
fn test_entry_access_tracking() {
let mut entry: CacheEntry<String, String, ()> =
CacheEntry::new("key1".to_string(), "value1".to_string());
let initial_time = entry.last_accessed;
std::thread::sleep(std::time::Duration::from_millis(10));
entry.record_access();
assert_eq!(entry.access_count, 1);
assert!(entry.last_accessed > initial_time);
entry.record_access();
assert_eq!(entry.access_count, 2);
}
}