1use crate::errors::Result;
10use crate::types::OcrResult;
11use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13use std::path::PathBuf;
14use std::time::Duration;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum EvictionPolicy {
19 Lru,
21 Lfu,
23 Fifo,
25}
26
27#[async_trait]
29pub trait CacheBackend: Send + Sync {
30 async fn get(&self, key: &str) -> Result<Option<OcrResult>>;
32
33 async fn put(&self, key: &str, value: &OcrResult, ttl: Option<Duration>) -> Result<()>;
35
36 async fn contains(&self, key: &str) -> Result<bool>;
38
39 async fn remove(&self, key: &str) -> Result<()>;
41
42 async fn clear(&self) -> Result<()>;
44
45 async fn stats(&self) -> Result<CacheStats>;
47
48 async fn len(&self) -> Result<usize>;
50
51 async fn is_empty(&self) -> Result<bool> {
53 Ok(self.len().await? == 0)
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct CacheStats {
60 pub total_entries: usize,
62 pub hits: u64,
64 pub misses: u64,
66 pub hit_rate: f64,
68 pub size_bytes: Option<u64>,
70 pub eviction_policy: EvictionPolicy,
72}
73
74impl CacheStats {
75 pub fn new(eviction_policy: EvictionPolicy) -> Self {
77 Self {
78 total_entries: 0,
79 hits: 0,
80 misses: 0,
81 hit_rate: 0.0,
82 size_bytes: None,
83 eviction_policy,
84 }
85 }
86
87 pub fn calculate_hit_rate(&mut self) {
89 let total = self.hits + self.misses;
90 self.hit_rate = if total > 0 {
91 self.hits as f64 / total as f64
92 } else {
93 0.0
94 };
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct RedisConfig {
101 pub url: String,
103 pub key_prefix: String,
105 pub default_ttl: Duration,
107 pub eviction_policy: EvictionPolicy,
109}
110
111impl Default for RedisConfig {
112 fn default() -> Self {
113 Self {
114 url: "redis://127.0.0.1:6379".to_string(),
115 key_prefix: "oxify:vision:".to_string(),
116 default_ttl: Duration::from_secs(3600),
117 eviction_policy: EvictionPolicy::Lru,
118 }
119 }
120}
121
122impl RedisConfig {
123 pub fn new(url: impl Into<String>) -> Self {
125 Self {
126 url: url.into(),
127 ..Default::default()
128 }
129 }
130
131 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
133 self.key_prefix = prefix.into();
134 self
135 }
136
137 pub fn with_ttl(mut self, ttl: Duration) -> Self {
139 self.default_ttl = ttl;
140 self
141 }
142
143 pub fn with_eviction_policy(mut self, policy: EvictionPolicy) -> Self {
145 self.eviction_policy = policy;
146 self
147 }
148}
149
150pub struct RedisBackend {
152 _config: RedisConfig,
153}
154
155impl RedisBackend {
156 pub async fn new(_config: RedisConfig) -> Result<Self> {
158 Err(crate::errors::VisionError::config(
159 "Redis backend not yet implemented - requires redis crate integration",
160 ))
161 }
162}
163
164#[async_trait]
165impl CacheBackend for RedisBackend {
166 async fn get(&self, _key: &str) -> Result<Option<OcrResult>> {
167 Err(crate::errors::VisionError::config("Redis not available"))
168 }
169
170 async fn put(&self, _key: &str, _value: &OcrResult, _ttl: Option<Duration>) -> Result<()> {
171 Err(crate::errors::VisionError::config("Redis not available"))
172 }
173
174 async fn contains(&self, _key: &str) -> Result<bool> {
175 Err(crate::errors::VisionError::config("Redis not available"))
176 }
177
178 async fn remove(&self, _key: &str) -> Result<()> {
179 Err(crate::errors::VisionError::config("Redis not available"))
180 }
181
182 async fn clear(&self) -> Result<()> {
183 Err(crate::errors::VisionError::config("Redis not available"))
184 }
185
186 async fn stats(&self) -> Result<CacheStats> {
187 Err(crate::errors::VisionError::config("Redis not available"))
188 }
189
190 async fn len(&self) -> Result<usize> {
191 Err(crate::errors::VisionError::config("Redis not available"))
192 }
193}
194
195#[derive(Debug, Clone)]
197pub struct SqliteConfig {
198 pub db_path: PathBuf,
200 pub max_entries: usize,
202 pub default_ttl: Duration,
204 pub eviction_policy: EvictionPolicy,
206}
207
208impl Default for SqliteConfig {
209 fn default() -> Self {
210 let cache_dir = crate::downloader::default_cache_dir();
211 Self {
212 db_path: cache_dir.join("cache.db"),
213 max_entries: 10000,
214 default_ttl: Duration::from_secs(86400), eviction_policy: EvictionPolicy::Lru,
216 }
217 }
218}
219
220impl SqliteConfig {
221 pub fn new(db_path: PathBuf) -> Self {
223 Self {
224 db_path,
225 ..Default::default()
226 }
227 }
228
229 pub fn with_max_entries(mut self, max: usize) -> Self {
231 self.max_entries = max;
232 self
233 }
234
235 pub fn with_ttl(mut self, ttl: Duration) -> Self {
237 self.default_ttl = ttl;
238 self
239 }
240
241 pub fn with_eviction_policy(mut self, policy: EvictionPolicy) -> Self {
243 self.eviction_policy = policy;
244 self
245 }
246}
247
248pub struct SqliteBackend {
250 _config: SqliteConfig,
251}
252
253impl SqliteBackend {
254 pub async fn new(_config: SqliteConfig) -> Result<Self> {
256 Err(crate::errors::VisionError::config(
257 "SQLite backend not yet implemented - requires rusqlite crate integration",
258 ))
259 }
260}
261
262#[async_trait]
263impl CacheBackend for SqliteBackend {
264 async fn get(&self, _key: &str) -> Result<Option<OcrResult>> {
265 Err(crate::errors::VisionError::config("SQLite not available"))
266 }
267
268 async fn put(&self, _key: &str, _value: &OcrResult, _ttl: Option<Duration>) -> Result<()> {
269 Err(crate::errors::VisionError::config("SQLite not available"))
270 }
271
272 async fn contains(&self, _key: &str) -> Result<bool> {
273 Err(crate::errors::VisionError::config("SQLite not available"))
274 }
275
276 async fn remove(&self, _key: &str) -> Result<()> {
277 Err(crate::errors::VisionError::config("SQLite not available"))
278 }
279
280 async fn clear(&self) -> Result<()> {
281 Err(crate::errors::VisionError::config("SQLite not available"))
282 }
283
284 async fn stats(&self) -> Result<CacheStats> {
285 Err(crate::errors::VisionError::config("SQLite not available"))
286 }
287
288 async fn len(&self) -> Result<usize> {
289 Err(crate::errors::VisionError::config("SQLite not available"))
290 }
291}
292
293pub struct PersistentCache {
295 backend: Box<dyn CacheBackend>,
296 default_ttl: Duration,
297}
298
299impl PersistentCache {
300 pub fn new(backend: Box<dyn CacheBackend>, default_ttl: Duration) -> Self {
302 Self {
303 backend,
304 default_ttl,
305 }
306 }
307
308 pub async fn redis(config: RedisConfig) -> Result<Self> {
310 let ttl = config.default_ttl;
311 let backend = RedisBackend::new(config).await?;
312 Ok(Self::new(Box::new(backend), ttl))
313 }
314
315 pub async fn sqlite(config: SqliteConfig) -> Result<Self> {
317 let ttl = config.default_ttl;
318 let backend = SqliteBackend::new(config).await?;
319 Ok(Self::new(Box::new(backend), ttl))
320 }
321
322 pub async fn get(&self, key: &str) -> Result<Option<OcrResult>> {
324 self.backend.get(key).await
325 }
326
327 pub async fn put(&self, key: &str, value: &OcrResult) -> Result<()> {
329 self.backend.put(key, value, Some(self.default_ttl)).await
330 }
331
332 pub async fn put_with_ttl(&self, key: &str, value: &OcrResult, ttl: Duration) -> Result<()> {
334 self.backend.put(key, value, Some(ttl)).await
335 }
336
337 pub async fn contains(&self, key: &str) -> Result<bool> {
339 self.backend.contains(key).await
340 }
341
342 pub async fn remove(&self, key: &str) -> Result<()> {
344 self.backend.remove(key).await
345 }
346
347 pub async fn clear(&self) -> Result<()> {
349 self.backend.clear().await
350 }
351
352 pub async fn stats(&self) -> Result<CacheStats> {
354 self.backend.stats().await
355 }
356
357 pub async fn len(&self) -> Result<usize> {
359 self.backend.len().await
360 }
361
362 pub async fn is_empty(&self) -> Result<bool> {
364 self.backend.is_empty().await
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_eviction_policy() {
374 let policies = vec![
375 EvictionPolicy::Lru,
376 EvictionPolicy::Lfu,
377 EvictionPolicy::Fifo,
378 ];
379
380 for policy in policies {
381 let _ = format!("{:?}", policy);
382 }
383 }
384
385 #[test]
386 fn test_cache_stats() {
387 let mut stats = CacheStats::new(EvictionPolicy::Lru);
388 assert_eq!(stats.total_entries, 0);
389 assert_eq!(stats.hits, 0);
390 assert_eq!(stats.misses, 0);
391 assert_eq!(stats.hit_rate, 0.0);
392
393 stats.hits = 80;
394 stats.misses = 20;
395 stats.calculate_hit_rate();
396 assert!((stats.hit_rate - 0.8).abs() < 0.001);
397 }
398
399 #[test]
400 fn test_redis_config() {
401 let config = RedisConfig::new("redis://localhost:6379")
402 .with_prefix("test:")
403 .with_ttl(Duration::from_secs(1800))
404 .with_eviction_policy(EvictionPolicy::Lfu);
405
406 assert_eq!(config.url, "redis://localhost:6379");
407 assert_eq!(config.key_prefix, "test:");
408 assert_eq!(config.default_ttl, Duration::from_secs(1800));
409 assert_eq!(config.eviction_policy, EvictionPolicy::Lfu);
410 }
411
412 #[test]
413 fn test_sqlite_config() {
414 let config = SqliteConfig::new(PathBuf::from("/tmp/test.db"))
415 .with_max_entries(5000)
416 .with_ttl(Duration::from_secs(7200))
417 .with_eviction_policy(EvictionPolicy::Fifo);
418
419 assert_eq!(config.db_path, PathBuf::from("/tmp/test.db"));
420 assert_eq!(config.max_entries, 5000);
421 assert_eq!(config.default_ttl, Duration::from_secs(7200));
422 assert_eq!(config.eviction_policy, EvictionPolicy::Fifo);
423 }
424
425 #[test]
426 fn test_redis_config_default() {
427 let config = RedisConfig::default();
428 assert!(config.url.contains("redis://"));
429 assert!(!config.key_prefix.is_empty());
430 }
431
432 #[test]
433 fn test_sqlite_config_default() {
434 let config = SqliteConfig::default();
435 assert!(config.db_path.to_string_lossy().contains("cache.db"));
436 assert!(config.max_entries > 0);
437 }
438
439 #[tokio::test]
440 async fn test_redis_backend_stub() {
441 let config = RedisConfig::default();
442 let result = RedisBackend::new(config).await;
443 assert!(result.is_err());
444 }
445
446 #[tokio::test]
447 async fn test_sqlite_backend_stub() {
448 let config = SqliteConfig::default();
449 let result = SqliteBackend::new(config).await;
450 assert!(result.is_err());
451 }
452
453 #[tokio::test]
454 async fn test_persistent_cache_redis_stub() {
455 let config = RedisConfig::default();
456 let result = PersistentCache::redis(config).await;
457 assert!(result.is_err());
458 }
459
460 #[tokio::test]
461 async fn test_persistent_cache_sqlite_stub() {
462 let config = SqliteConfig::default();
463 let result = PersistentCache::sqlite(config).await;
464 assert!(result.is_err());
465 }
466}