threatflux_cache/
entry.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fmt::Debug;
6use std::hash::Hash;
7
8#[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 pub key: K,
18 pub value: V,
20 pub metadata: M,
22 pub timestamp: DateTime<Utc>,
24 pub expiry: Option<DateTime<Utc>>,
26 pub access_count: u64,
28 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 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 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 pub fn with_metadata(key: K, value: V, metadata: M) -> Self {
66 Self::init(key, value, metadata)
67 }
68
69 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 pub fn is_expired(&self) -> bool {
83 self.expiry.is_some_and(|expiry| Utc::now() >= expiry)
84 }
85
86 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 pub fn age(&self) -> chrono::Duration {
94 Utc::now() - self.timestamp
95 }
96}
97
98pub trait EntryMetadata:
100 Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync + 'static
101{
102 fn execution_time_ms(&self) -> Option<u64> {
104 None
105 }
106
107 fn size_bytes(&self) -> Option<u64> {
109 None
110 }
111
112 fn category(&self) -> Option<&str> {
114 None
115 }
116}
117
118impl EntryMetadata for () {}
120
121#[derive(Debug, Clone, Serialize, Deserialize, Default)]
123pub struct BasicMetadata {
124 pub execution_time_ms: Option<u64>,
126 pub size_bytes: Option<u64>,
128 pub category: Option<String>,
130 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}