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