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}