cache_kit/strategy.rs
1//! Cache strategies and decision logic for fetch operations.
2//!
3//! This module defines the different strategies for accessing cached data and provides
4//! contextual information about cache operations.
5//!
6//! # Overview
7//!
8//! Cache-kit uses an enum-based strategy pattern to replace ad-hoc boolean flags.
9//! This makes cache behavior explicit and type-safe.
10//!
11//! # The Four Strategies
12//!
13//! Every cache operation uses one of four strategies:
14//!
15//! ```
16//! use cache_kit::strategy::CacheStrategy;
17//!
18//! // 1. Fresh - Use cache only
19//! let _s = CacheStrategy::Fresh;
20//!
21//! // 2. Refresh - Cache-first with DB fallback (default)
22//! let _s = CacheStrategy::Refresh;
23//!
24//! // 3. Invalidate - Clear cache, always fetch fresh
25//! let _s = CacheStrategy::Invalidate;
26//!
27//! // 4. Bypass - Skip cache entirely
28//! let _s = CacheStrategy::Bypass;
29//! ```
30//!
31//! # Decision Tree
32//!
33//! ```text
34//! Do you have data in cache?
35//! ├─ YES, and you trust it
36//! │ └─ Use: Fresh or Refresh
37//! │
38//! ├─ NO, or it's possibly stale
39//! │ └─ Use: Invalidate or Refresh
40//! │
41//! └─ You don't want caching now
42//! └─ Use: Bypass
43//! ```
44//!
45//! # When to Use Each Strategy
46//!
47//! | Strategy | Cache Hit | Cache Miss | Use Case |
48//! |----------|-----------|-----------|----------|
49//! | **Fresh** | Return | Return None | Assume data cached; miss is error |
50//! | **Refresh** | Return | DB fallback | Default; prefer cache, ensure availability |
51//! | **Invalidate** | Delete | Fetch DB | After mutations; need fresh data |
52//! | **Bypass** | Ignore | DB always | Testing or temporary disable |
53//!
54//! # Examples by Scenario
55//!
56//! These examples show typical usage patterns for different strategies.
57//! See the module tests for complete runnable examples.
58//!
59//! # Trade-offs
60//!
61//! - **Refresh** (default): Best for most cases. Balances performance and consistency.
62//! - **Fresh**: Fastest if hit, but fails on cache miss.
63//! - **Invalidate**: Ensures freshness but increases DB load after mutations.
64//! - **Bypass**: Simplest for testing, but defeats caching benefits.
65
66use std::time::Duration;
67
68/// Strategy enum controlling cache fetch/invalidation behavior.
69///
70/// Replaces boolean flags with explicit, type-safe options.
71///
72/// # Examples
73///
74/// ```
75/// use cache_kit::strategy::CacheStrategy;
76///
77/// // Use cache only
78/// let _strategy = CacheStrategy::Fresh;
79///
80/// // Try cache, fallback to database
81/// let _strategy = CacheStrategy::Refresh;
82///
83/// // Clear cache and refresh from database
84/// let _strategy = CacheStrategy::Invalidate;
85///
86/// // Skip cache entirely
87/// let _strategy = CacheStrategy::Bypass;
88/// ```
89#[derive(Clone, Debug, PartialEq, Eq, Default)]
90pub enum CacheStrategy {
91 /// **Fresh**: Try cache only, no fallback to database.
92 ///
93 /// Use when: You know data should be in cache, and miss is an error condition.
94 ///
95 /// Flow:
96 /// 1. Check cache
97 /// 2. If hit: return cached value
98 /// 3. If miss: return None (don't hit database)
99 Fresh,
100
101 /// **Refresh**: Try cache first, fallback to database on miss.
102 ///
103 /// Use when: Default behavior, prefer cache but ensure data availability.
104 ///
105 /// Flow:
106 /// 1. Check cache
107 /// 2. If hit: return cached value
108 /// 3. If miss: fetch from database
109 /// 4. Store in cache
110 /// 5. Return value
111 #[default]
112 Refresh,
113
114 /// **Invalidate**: Mark cache as invalid and refresh from database.
115 ///
116 /// Use when: You know cache is stale and need fresh data.
117 /// Typical use: After update/mutation operations.
118 ///
119 /// Flow:
120 /// 1. Delete from cache
121 /// 2. Fetch from database
122 /// 3. Store in cache
123 /// 4. Return value
124 Invalidate,
125
126 /// **Bypass**: Ignore cache entirely, always fetch from database.
127 ///
128 /// Use when: Cache is temporarily disabled or for specific read-through scenarios.
129 ///
130 /// Flow:
131 /// 1. Fetch from database
132 /// 2. Store in cache (for others)
133 /// 3. Return value
134 Bypass,
135}
136
137impl std::fmt::Display for CacheStrategy {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 match self {
140 CacheStrategy::Fresh => write!(f, "Fresh"),
141 CacheStrategy::Refresh => write!(f, "Refresh"),
142 CacheStrategy::Invalidate => write!(f, "Invalidate"),
143 CacheStrategy::Bypass => write!(f, "Bypass"),
144 }
145 }
146}
147
148/// Context information for cache operations.
149#[derive(Clone, Debug)]
150pub struct CacheContext {
151 /// Cache key
152 pub key: String,
153
154 /// Whether value exists in cache
155 pub is_cached: bool,
156
157 /// Remaining TTL if cached
158 pub ttl_remaining: Option<Duration>,
159
160 /// Timestamp of last cache update
161 pub cached_at: Option<std::time::Instant>,
162
163 /// Custom metadata (user-provided)
164 pub metadata: std::collections::HashMap<String, String>,
165}
166
167impl CacheContext {
168 pub fn new(key: String) -> Self {
169 CacheContext {
170 key,
171 is_cached: false,
172 ttl_remaining: None,
173 cached_at: None,
174 metadata: std::collections::HashMap::new(),
175 }
176 }
177
178 pub fn with_cached(mut self, is_cached: bool) -> Self {
179 self.is_cached = is_cached;
180 self
181 }
182
183 pub fn with_ttl(mut self, ttl: Duration) -> Self {
184 self.ttl_remaining = Some(ttl);
185 self
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_strategy_display() {
195 assert_eq!(CacheStrategy::Fresh.to_string(), "Fresh");
196 assert_eq!(CacheStrategy::Refresh.to_string(), "Refresh");
197 assert_eq!(CacheStrategy::Invalidate.to_string(), "Invalidate");
198 assert_eq!(CacheStrategy::Bypass.to_string(), "Bypass");
199 }
200
201 #[test]
202 fn test_strategy_default() {
203 assert_eq!(CacheStrategy::default(), CacheStrategy::Refresh);
204 }
205
206 #[test]
207 fn test_strategy_equality() {
208 assert_eq!(CacheStrategy::Fresh, CacheStrategy::Fresh);
209 assert_ne!(CacheStrategy::Fresh, CacheStrategy::Refresh);
210 }
211
212 #[test]
213 fn test_cache_context_builder() {
214 let ctx = CacheContext::new("test_key".to_string())
215 .with_cached(true)
216 .with_ttl(Duration::from_secs(300));
217
218 assert_eq!(ctx.key, "test_key");
219 assert!(ctx.is_cached);
220 assert_eq!(ctx.ttl_remaining, Some(Duration::from_secs(300)));
221 }
222}