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 pub content_type: ContentType,
23 pub expires_at: Option<SystemTime>,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub enum ContentType {
33 Bytes,
35 Text,
37 Json,
39 Xml,
41}
42
43#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
47pub struct CacheStats {
48 pub hits: u64,
50 pub misses: u64,
52 pub evictions: u64,
54 pub entries: u64,
56 pub peek_stale_served: u64,
58 pub invalidations: u64,
60 pub bytes: Option<u64>,
62}
63
64#[async_trait::async_trait]
69pub trait CacheRepository: Send + Sync + std::fmt::Debug + 'static {
70 fn name(&self) -> &str;
72
73 async fn get(&self, key: &str) -> Result<Option<CacheEntry>, CamelError>;
78
79 async fn set(
84 &self,
85 key: &str,
86 value: CacheEntry,
87 ttl: Option<Duration>,
88 ) -> Result<(), CamelError>;
89
90 async fn peek_stale(&self, key: &str) -> Result<Option<CacheEntry>, CamelError>;
94
95 async fn invalidate(&self, key: &str) -> Result<(), CamelError>;
97
98 async fn clear(&self) -> Result<(), CamelError>;
100
101 async fn invalidate_prefix(&self, prefix: &str) -> Result<u64, CamelError> {
107 let _ = prefix;
108 Err(CamelError::Config(format!(
109 "cache backend '{}' does not support invalidate_prefix (no key iteration)",
110 self.name()
111 )))
112 }
113
114 async fn stats(&self) -> CacheStats {
118 CacheStats::default()
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn cache_entry_construction() {
128 let entry = CacheEntry {
129 bytes: vec![b'x'],
130 content_type: ContentType::Bytes,
131 expires_at: None,
132 };
133 assert_eq!(entry.bytes.len(), 1);
134 assert_eq!(entry.content_type, ContentType::Bytes);
135 }
136
137 #[test]
138 fn cache_stats_default() {
139 assert_eq!(
140 CacheStats::default(),
141 CacheStats {
142 hits: 0,
143 misses: 0,
144 evictions: 0,
145 entries: 0,
146 peek_stale_served: 0,
147 invalidations: 0,
148 bytes: None,
149 }
150 );
151 }
152
153 #[test]
154 fn cache_stats_serialize_round_trip() {
155 let stats = CacheStats {
156 hits: 2,
157 misses: 1,
158 evictions: 0,
159 entries: 3,
160 peek_stale_served: 4,
161 invalidations: 1,
162 bytes: None,
163 };
164 let json = serde_json::to_string(&stats).unwrap();
165 assert!(json.contains("\"peek_stale_served\""));
166 assert!(json.contains("\"bytes\":null"));
167 let back: CacheStats = serde_json::from_str(&json).unwrap();
168 assert_eq!(stats, back);
169 }
170
171 struct NoIter;
174
175 impl std::fmt::Debug for NoIter {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 f.write_str("NoIter")
178 }
179 }
180
181 #[async_trait::async_trait]
182 impl CacheRepository for NoIter {
183 fn name(&self) -> &str {
184 "noiter"
185 }
186
187 async fn get(&self, _key: &str) -> Result<Option<CacheEntry>, CamelError> {
188 Ok(None)
189 }
190
191 async fn set(
192 &self,
193 _key: &str,
194 _value: CacheEntry,
195 _ttl: Option<Duration>,
196 ) -> Result<(), CamelError> {
197 Ok(())
198 }
199
200 async fn peek_stale(&self, _key: &str) -> Result<Option<CacheEntry>, CamelError> {
201 Ok(None)
202 }
203
204 async fn invalidate(&self, _key: &str) -> Result<(), CamelError> {
205 Ok(())
206 }
207
208 async fn clear(&self) -> Result<(), CamelError> {
209 Ok(())
210 }
211 }
212
213 #[tokio::test]
214 async fn default_invalidate_prefix_returns_err_naming_backend() {
215 let repo = NoIter;
216 let err = repo.invalidate_prefix("ns:").await.unwrap_err();
217 assert!(
218 format!("{err}").contains("noiter"),
219 "error must name the backend, got: {err}"
220 );
221 }
222
223 #[tokio::test]
224 async fn default_async_stats_returns_zeroed() {
225 let stats = NoIter.stats().await;
226 assert_eq!(stats.hits, 0);
227 assert_eq!(stats.misses, 0);
228 assert_eq!(stats.evictions, 0);
229 assert_eq!(stats.entries, 0);
230 assert_eq!(stats.peek_stale_served, 0);
231 assert_eq!(stats.invalidations, 0);
232 assert_eq!(stats.bytes, None);
233 }
234}