oxify_connect_vision/
persistent_cache.rs

1//! Persistent caching backends for OCR results.
2//!
3//! This module provides persistent storage options for OCR results:
4//! - Redis backend for distributed caching
5//! - SQLite backend for local persistent cache
6//! - Cache statistics and metrics
7//! - Configurable eviction policies (LRU, LFU)
8
9use 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/// Cache eviction policy.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum EvictionPolicy {
19    /// Least Recently Used
20    Lru,
21    /// Least Frequently Used
22    Lfu,
23    /// First In First Out
24    Fifo,
25}
26
27/// Cache backend trait for persistent storage.
28#[async_trait]
29pub trait CacheBackend: Send + Sync {
30    /// Get a cached result by key.
31    async fn get(&self, key: &str) -> Result<Option<OcrResult>>;
32
33    /// Store a result in cache.
34    async fn put(&self, key: &str, value: &OcrResult, ttl: Option<Duration>) -> Result<()>;
35
36    /// Check if a key exists in cache.
37    async fn contains(&self, key: &str) -> Result<bool>;
38
39    /// Remove a key from cache.
40    async fn remove(&self, key: &str) -> Result<()>;
41
42    /// Clear all cached entries.
43    async fn clear(&self) -> Result<()>;
44
45    /// Get cache statistics.
46    async fn stats(&self) -> Result<CacheStats>;
47
48    /// Get the number of entries in cache.
49    async fn len(&self) -> Result<usize>;
50
51    /// Check if cache is empty.
52    async fn is_empty(&self) -> Result<bool> {
53        Ok(self.len().await? == 0)
54    }
55}
56
57/// Cache statistics.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct CacheStats {
60    /// Total number of entries
61    pub total_entries: usize,
62    /// Cache hits
63    pub hits: u64,
64    /// Cache misses
65    pub misses: u64,
66    /// Hit rate (0.0 to 1.0)
67    pub hit_rate: f64,
68    /// Total size in bytes (if available)
69    pub size_bytes: Option<u64>,
70    /// Eviction policy
71    pub eviction_policy: EvictionPolicy,
72}
73
74impl CacheStats {
75    /// Create new empty stats.
76    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    /// Calculate hit rate.
88    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/// Redis cache backend configuration.
99#[derive(Debug, Clone)]
100pub struct RedisConfig {
101    /// Redis connection URL
102    pub url: String,
103    /// Key prefix for cache entries
104    pub key_prefix: String,
105    /// Default TTL for entries
106    pub default_ttl: Duration,
107    /// Eviction policy
108    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    /// Create a new Redis configuration.
124    pub fn new(url: impl Into<String>) -> Self {
125        Self {
126            url: url.into(),
127            ..Default::default()
128        }
129    }
130
131    /// Set key prefix.
132    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
133        self.key_prefix = prefix.into();
134        self
135    }
136
137    /// Set default TTL.
138    pub fn with_ttl(mut self, ttl: Duration) -> Self {
139        self.default_ttl = ttl;
140        self
141    }
142
143    /// Set eviction policy.
144    pub fn with_eviction_policy(mut self, policy: EvictionPolicy) -> Self {
145        self.eviction_policy = policy;
146        self
147    }
148}
149
150/// Redis cache backend (stub - full implementation requires redis crate).
151pub struct RedisBackend {
152    _config: RedisConfig,
153}
154
155impl RedisBackend {
156    /// Create a new Redis backend.
157    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/// SQLite cache backend configuration.
196#[derive(Debug, Clone)]
197pub struct SqliteConfig {
198    /// Database file path
199    pub db_path: PathBuf,
200    /// Maximum number of entries
201    pub max_entries: usize,
202    /// Default TTL for entries
203    pub default_ttl: Duration,
204    /// Eviction policy
205    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), // 24 hours
215            eviction_policy: EvictionPolicy::Lru,
216        }
217    }
218}
219
220impl SqliteConfig {
221    /// Create a new SQLite configuration.
222    pub fn new(db_path: PathBuf) -> Self {
223        Self {
224            db_path,
225            ..Default::default()
226        }
227    }
228
229    /// Set maximum entries.
230    pub fn with_max_entries(mut self, max: usize) -> Self {
231        self.max_entries = max;
232        self
233    }
234
235    /// Set default TTL.
236    pub fn with_ttl(mut self, ttl: Duration) -> Self {
237        self.default_ttl = ttl;
238        self
239    }
240
241    /// Set eviction policy.
242    pub fn with_eviction_policy(mut self, policy: EvictionPolicy) -> Self {
243        self.eviction_policy = policy;
244        self
245    }
246}
247
248/// SQLite cache backend (stub - full implementation requires rusqlite crate).
249pub struct SqliteBackend {
250    _config: SqliteConfig,
251}
252
253impl SqliteBackend {
254    /// Create a new SQLite backend.
255    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
293/// Persistent cache wrapper that uses a backend.
294pub struct PersistentCache {
295    backend: Box<dyn CacheBackend>,
296    default_ttl: Duration,
297}
298
299impl PersistentCache {
300    /// Create a cache with a custom backend.
301    pub fn new(backend: Box<dyn CacheBackend>, default_ttl: Duration) -> Self {
302        Self {
303            backend,
304            default_ttl,
305        }
306    }
307
308    /// Create a Redis-backed cache.
309    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    /// Create a SQLite-backed cache.
316    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    /// Get a cached result.
323    pub async fn get(&self, key: &str) -> Result<Option<OcrResult>> {
324        self.backend.get(key).await
325    }
326
327    /// Store a result in cache.
328    pub async fn put(&self, key: &str, value: &OcrResult) -> Result<()> {
329        self.backend.put(key, value, Some(self.default_ttl)).await
330    }
331
332    /// Store a result with custom TTL.
333    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    /// Check if a key exists.
338    pub async fn contains(&self, key: &str) -> Result<bool> {
339        self.backend.contains(key).await
340    }
341
342    /// Remove a key.
343    pub async fn remove(&self, key: &str) -> Result<()> {
344        self.backend.remove(key).await
345    }
346
347    /// Clear all entries.
348    pub async fn clear(&self) -> Result<()> {
349        self.backend.clear().await
350    }
351
352    /// Get cache statistics.
353    pub async fn stats(&self) -> Result<CacheStats> {
354        self.backend.stats().await
355    }
356
357    /// Get number of entries.
358    pub async fn len(&self) -> Result<usize> {
359        self.backend.len().await
360    }
361
362    /// Check if cache is empty.
363    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}