Skip to main content

camel_api/
cache.rs

1//! # Cache Repository
2//!
3//! Pluggable cache backend abstraction for the Caching EIP.
4//!
5//! ## Contract
6//!
7//! Per ADR-0023 Contract C1, backend failures (network errors, timeouts, storage
8//! unavailability) MUST surface as `Err(CamelError)`, never as a silent miss.
9//! A `get` that returns `Ok(None)` means the key is definitively absent or
10//! expired — not "I couldn't check."
11
12use std::time::{Duration, SystemTime};
13
14use crate::CamelError;
15
16/// A cached entry with its content type and optional expiry.
17#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
18pub struct CacheEntry {
19    /// The raw cached bytes.
20    pub bytes: Vec<u8>,
21    /// The content type of the cached data.
22    pub content_type: ContentType,
23    /// When this entry expires (if set). `None` means no expiry.
24    pub expires_at: Option<SystemTime>,
25}
26
27/// The content type classification for a cached entry.
28///
29/// exhaustive-by-contract: closed 4-variant set; out-of-crate CacheService matches all variants
30/// for content_type→Body reconstruction (ADR-0049 §Exceptions).
31#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub enum ContentType {
33    /// Arbitrary binary data.
34    Bytes,
35    /// UTF-8 text.
36    Text,
37    /// JSON-encoded data.
38    Json,
39    /// XML-encoded data.
40    Xml,
41}
42
43/// Cache usage statistics.
44///
45/// Backends construct this with struct literals — NOT `#[non_exhaustive]`.
46#[derive(Debug, Clone, Default, PartialEq)]
47pub struct CacheStats {
48    /// Number of cache hits.
49    pub hits: u64,
50    /// Number of cache misses.
51    pub misses: u64,
52    /// Number of entries evicted.
53    pub evictions: u64,
54    /// Current number of entries in the cache.
55    pub entries: u64,
56}
57
58/// Pluggable cache backend.
59///
60/// Implementations MUST be `Send + Sync + 'static` and propagate all backend
61/// failures as `Err(CamelError)` (Contract C1).
62#[async_trait::async_trait]
63pub trait CacheRepository: Send + Sync + std::fmt::Debug + 'static {
64    /// A human-readable name for this cache backend.
65    fn name(&self) -> &str;
66
67    /// Retrieve a value by key.
68    ///
69    /// Returns `Ok(None)` if the key is absent or expired. All backend failures
70    /// MUST be returned as `Err(CamelError)`.
71    async fn get(&self, key: &str) -> Result<Option<CacheEntry>, CamelError>;
72
73    /// Store a value with an optional TTL.
74    ///
75    /// The implementation computes `expires_at` from `ttl` and stores it in the
76    /// `CacheEntry`. If `ttl` is `None`, the entry has no expiry.
77    async fn set(
78        &self,
79        key: &str,
80        value: CacheEntry,
81        ttl: Option<Duration>,
82    ) -> Result<(), CamelError>;
83
84    /// Peek at a stale (expired but not yet evicted) entry.
85    ///
86    /// Returns `Ok(None)` if the key is absent or not yet expired.
87    async fn peek_stale(&self, key: &str) -> Result<Option<CacheEntry>, CamelError>;
88
89    /// Remove a specific key from the cache.
90    async fn invalidate(&self, key: &str) -> Result<(), CamelError>;
91
92    /// Remove all entries from the cache.
93    async fn clear(&self) -> Result<(), CamelError>;
94
95    /// Return current cache statistics.
96    ///
97    /// Default implementation returns zeroed stats.
98    fn stats(&self) -> CacheStats {
99        CacheStats::default()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn cache_entry_construction() {
109        let entry = CacheEntry {
110            bytes: vec![b'x'],
111            content_type: ContentType::Bytes,
112            expires_at: None,
113        };
114        assert_eq!(entry.bytes.len(), 1);
115        assert_eq!(entry.content_type, ContentType::Bytes);
116    }
117
118    #[test]
119    fn cache_stats_default() {
120        assert_eq!(
121            CacheStats::default(),
122            CacheStats {
123                hits: 0,
124                misses: 0,
125                evictions: 0,
126                entries: 0,
127            }
128        );
129    }
130}