Skip to main content

threatflux_cache/
entry.rs

1//! Cache entry types and metadata traits
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fmt::Debug;
6use std::hash::Hash;
7
8/// A cache entry containing a key-value pair with metadata
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CacheEntry<K, V, M = ()>
11where
12    K: Clone + Hash + Eq,
13    V: Clone,
14    M: Clone,
15{
16    /// The cache key
17    pub key: K,
18    /// The cached value
19    pub value: V,
20    /// Optional metadata associated with the entry
21    pub metadata: M,
22    /// Timestamp when the entry was created
23    pub timestamp: DateTime<Utc>,
24    /// Optional expiry time for TTL-based eviction
25    pub expiry: Option<DateTime<Utc>>,
26    /// Number of times this entry has been accessed
27    pub access_count: u64,
28    /// Last access timestamp
29    pub last_accessed: DateTime<Utc>,
30}
31
32impl<K, V, M> CacheEntry<K, V, M>
33where
34    K: Clone + Hash + Eq,
35    V: Clone,
36    M: Clone + Default,
37{
38    /// Create a new cache entry with default metadata
39    pub fn new(key: K, value: V) -> Self {
40        Self::init(key, value, M::default())
41    }
42}
43
44impl<K, V, M> CacheEntry<K, V, M>
45where
46    K: Clone + Hash + Eq,
47    V: Clone,
48    M: Clone,
49{
50    /// Internal constructor used by `new` and `with_metadata`
51    fn init(key: K, value: V, metadata: M) -> Self {
52        let now = Utc::now();
53        Self {
54            key,
55            value,
56            metadata,
57            timestamp: now,
58            expiry: None,
59            access_count: 0,
60            last_accessed: now,
61        }
62    }
63
64    /// Create a new cache entry with metadata
65    pub fn with_metadata(key: K, value: V, metadata: M) -> Self {
66        Self::init(key, value, metadata)
67    }
68
69    /// Set expiry time for the entry
70    pub fn with_ttl(mut self, ttl: chrono::Duration) -> Self {
71        self.expiry = Some(self.timestamp.checked_add_signed(ttl).unwrap_or_else(|| {
72            if ttl < chrono::Duration::zero() {
73                DateTime::<Utc>::MIN_UTC
74            } else {
75                DateTime::<Utc>::MAX_UTC
76            }
77        }));
78        self
79    }
80
81    /// Check if the entry has expired
82    pub fn is_expired(&self) -> bool {
83        self.expiry.is_some_and(|expiry| Utc::now() >= expiry)
84    }
85
86    /// Update access statistics
87    pub fn record_access(&mut self) {
88        self.access_count = self.access_count.saturating_add(1);
89        self.last_accessed = Utc::now();
90    }
91
92    /// Get the age of the entry
93    pub fn age(&self) -> chrono::Duration {
94        Utc::now() - self.timestamp
95    }
96}
97
98/// Trait for cache entry metadata
99pub trait EntryMetadata:
100    Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync + 'static
101{
102    /// Get execution time in milliseconds if applicable
103    fn execution_time_ms(&self) -> Option<u64> {
104        None
105    }
106
107    /// Get the size of the cached data if applicable
108    fn size_bytes(&self) -> Option<u64> {
109        None
110    }
111
112    /// Get a category or type identifier
113    fn category(&self) -> Option<&str> {
114        None
115    }
116}
117
118/// Empty metadata implementation
119impl EntryMetadata for () {}
120
121/// Simple metadata implementation with common fields
122#[derive(Debug, Clone, Serialize, Deserialize, Default)]
123pub struct BasicMetadata {
124    /// Execution time in milliseconds
125    pub execution_time_ms: Option<u64>,
126    /// Size in bytes
127    pub size_bytes: Option<u64>,
128    /// Category or type
129    pub category: Option<String>,
130    /// Additional tags
131    pub tags: Vec<String>,
132}
133
134impl EntryMetadata for BasicMetadata {
135    fn execution_time_ms(&self) -> Option<u64> {
136        self.execution_time_ms
137    }
138
139    fn size_bytes(&self) -> Option<u64> {
140        self.size_bytes
141    }
142
143    fn category(&self) -> Option<&str> {
144        self.category.as_deref()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    fn sample_entry() -> CacheEntry<String, String, ()> {
153        CacheEntry::new("key1".to_string(), "value1".to_string())
154    }
155
156    #[test]
157    fn test_cache_entry_creation() {
158        let entry = sample_entry();
159        assert_eq!(entry.key, "key1");
160        assert_eq!(entry.value, "value1");
161        assert_eq!(entry.access_count, 0);
162        assert!(!entry.is_expired());
163    }
164
165    #[test]
166    fn test_cache_entry_ttl() {
167        let entry = sample_entry().with_ttl(chrono::Duration::seconds(60));
168
169        assert!(entry.expiry.is_some());
170        assert!(!entry.is_expired());
171    }
172
173    #[test]
174    fn test_cache_entry_metadata() {
175        let metadata = BasicMetadata {
176            execution_time_ms: Some(100),
177            size_bytes: Some(1024),
178            category: Some("test".to_string()),
179            tags: vec!["tag1".to_string()],
180        };
181
182        let entry = CacheEntry::with_metadata("key1".to_string(), "value1".to_string(), metadata);
183        assert_eq!(entry.metadata.execution_time_ms(), Some(100));
184        assert_eq!(entry.metadata.size_bytes(), Some(1024));
185        assert_eq!(entry.metadata.category(), Some("test"));
186    }
187
188    #[test]
189    fn test_entry_access_tracking() {
190        let mut entry = sample_entry();
191        entry.last_accessed = Utc::now() - chrono::Duration::seconds(1);
192        let initial_time = entry.last_accessed;
193
194        entry.record_access();
195        assert_eq!(entry.access_count, 1);
196        assert!(entry.last_accessed > initial_time);
197
198        entry.record_access();
199        assert_eq!(entry.access_count, 2);
200
201        entry.access_count = u64::MAX;
202        entry.record_access();
203        assert_eq!(entry.access_count, u64::MAX);
204    }
205
206    #[test]
207    fn test_entry_age() {
208        let mut entry = sample_entry();
209        entry.timestamp = Utc::now() - chrono::Duration::seconds(1);
210        assert!(entry.age() > chrono::Duration::zero());
211    }
212}