1use std::time::{Duration, SystemTime};
13
14use crate::CamelError;
15
16#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
18pub struct CacheEntry {
19 pub bytes: Vec<u8>,
21 #[serde(default)]
23 pub payload_path: Option<String>,
24 pub content_type: ContentType,
26 pub expires_at: Option<SystemTime>,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
35pub enum ContentType {
36 Bytes,
38 Text,
40 Json,
42 Xml,
44}
45
46#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
50pub struct CacheStats {
51 pub hits: u64,
53 pub misses: u64,
55 pub evictions: u64,
57 pub entries: u64,
59 pub peek_stale_served: u64,
61 pub invalidations: u64,
63 pub bytes: Option<u64>,
65}
66
67#[async_trait::async_trait]
72pub trait CacheRepository: Send + Sync + std::fmt::Debug + 'static {
73 fn name(&self) -> &str;
75
76 async fn get(&self, key: &str) -> Result<Option<CacheEntry>, CamelError>;
81
82 async fn set(
87 &self,
88 key: &str,
89 value: CacheEntry,
90 ttl: Option<Duration>,
91 ) -> Result<(), CamelError>;
92
93 async fn peek_stale(&self, key: &str) -> Result<Option<CacheEntry>, CamelError>;
97
98 async fn peek_row_silent(&self, _key: &str) -> Result<Option<CacheEntry>, CamelError> {
108 Ok(None)
109 }
110
111 async fn invalidate(&self, key: &str) -> Result<(), CamelError>;
113
114 async fn clear(&self) -> Result<(), CamelError>;
116
117 async fn invalidate_prefix(&self, prefix: &str) -> Result<u64, CamelError> {
123 let _ = prefix;
124 Err(CamelError::Config(format!(
125 "cache backend '{}' does not support invalidate_prefix (no key iteration)",
126 self.name()
127 )))
128 }
129
130 async fn stats(&self) -> CacheStats {
134 CacheStats::default()
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn cache_entry_construction() {
144 let entry = CacheEntry {
145 bytes: vec![b'x'],
146 payload_path: None,
147 content_type: ContentType::Bytes,
148 expires_at: None,
149 };
150 assert_eq!(entry.bytes.len(), 1);
151 assert_eq!(entry.content_type, ContentType::Bytes);
152 }
153
154 #[test]
155 fn legacy_json_without_payload_path_deserializes_as_none() {
156 let json = r#"{"bytes":[1,2,3],"content_type":"Bytes","expires_at":null}"#;
157 let entry: CacheEntry = serde_json::from_str(json).unwrap();
158 assert_eq!(entry.payload_path, None);
159 assert_eq!(entry.bytes, vec![1, 2, 3]);
160 }
161
162 #[test]
163 fn payload_path_round_trips_through_serde() {
164 let entry = CacheEntry {
165 bytes: vec![1, 2, 3],
166 payload_path: Some("abc.blob".into()),
167 content_type: ContentType::Bytes,
168 expires_at: None,
169 };
170 let json = serde_json::to_string(&entry).unwrap();
171 assert!(json.contains("\"payload_path\":\"abc.blob\""));
172 let back: CacheEntry = serde_json::from_str(&json).unwrap();
173 assert_eq!(back.payload_path, Some("abc.blob".to_string()));
174 }
175
176 #[test]
177 fn cache_stats_default() {
178 assert_eq!(
179 CacheStats::default(),
180 CacheStats {
181 hits: 0,
182 misses: 0,
183 evictions: 0,
184 entries: 0,
185 peek_stale_served: 0,
186 invalidations: 0,
187 bytes: None,
188 }
189 );
190 }
191
192 #[test]
193 fn cache_stats_serialize_round_trip() {
194 let stats = CacheStats {
195 hits: 2,
196 misses: 1,
197 evictions: 0,
198 entries: 3,
199 peek_stale_served: 4,
200 invalidations: 1,
201 bytes: None,
202 };
203 let json = serde_json::to_string(&stats).unwrap();
204 assert!(json.contains("\"peek_stale_served\""));
205 assert!(json.contains("\"bytes\":null"));
206 let back: CacheStats = serde_json::from_str(&json).unwrap();
207 assert_eq!(stats, back);
208 }
209
210 struct NoIter;
213
214 impl std::fmt::Debug for NoIter {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 f.write_str("NoIter")
217 }
218 }
219
220 #[async_trait::async_trait]
221 impl CacheRepository for NoIter {
222 fn name(&self) -> &str {
223 "noiter"
224 }
225
226 async fn get(&self, _key: &str) -> Result<Option<CacheEntry>, CamelError> {
227 Ok(None)
228 }
229
230 async fn set(
231 &self,
232 _key: &str,
233 _value: CacheEntry,
234 _ttl: Option<Duration>,
235 ) -> Result<(), CamelError> {
236 Ok(())
237 }
238
239 async fn peek_stale(&self, _key: &str) -> Result<Option<CacheEntry>, CamelError> {
240 Ok(None)
241 }
242
243 async fn invalidate(&self, _key: &str) -> Result<(), CamelError> {
244 Ok(())
245 }
246
247 async fn clear(&self) -> Result<(), CamelError> {
248 Ok(())
249 }
250 }
251
252 #[tokio::test]
253 async fn default_invalidate_prefix_returns_err_naming_backend() {
254 let repo = NoIter;
255 let err = repo.invalidate_prefix("ns:").await.unwrap_err();
256 assert!(
257 format!("{err}").contains("noiter"),
258 "error must name the backend, got: {err}"
259 );
260 }
261
262 #[tokio::test]
263 async fn default_async_stats_returns_zeroed() {
264 let stats = NoIter.stats().await;
265 assert_eq!(stats.hits, 0);
266 assert_eq!(stats.misses, 0);
267 assert_eq!(stats.evictions, 0);
268 assert_eq!(stats.entries, 0);
269 assert_eq!(stats.peek_stale_served, 0);
270 assert_eq!(stats.invalidations, 0);
271 assert_eq!(stats.bytes, None);
272 }
273}