cache_kit/service.rs
1//! High-level cache service for web applications.
2//!
3//! Provides a convenient wrapper around CacheExpander with Arc for easy sharing.
4
5use crate::backend::CacheBackend;
6use crate::entity::CacheEntity;
7use crate::error::Result;
8use crate::expander::{CacheExpander, OperationConfig};
9use crate::feed::CacheFeed;
10use crate::observability::CacheMetrics;
11use crate::repository::DataRepository;
12use crate::strategy::CacheStrategy;
13use std::str::FromStr;
14use std::sync::Arc;
15
16/// High-level cache service for web applications.
17///
18/// Wraps `CacheExpander` in `Arc` for easy sharing across threads without
19/// requiring external `Arc<Mutex<>>` wrappers.
20///
21/// # Design
22///
23/// Since `CacheBackend` implementations use interior mutability (RwLock, Mutex),
24/// and `CacheExpander` now uses `&self` methods, we can safely wrap it in `Arc`
25/// without needing an additional `Mutex`.
26///
27/// # Example
28///
29/// ```ignore
30/// use cache_kit::{CacheService, backend::InMemoryBackend};
31///
32/// // Create service (can be shared across threads)
33/// let cache = CacheService::new(InMemoryBackend::new());
34///
35/// // In your web service struct
36/// pub struct UserService {
37/// cache: CacheService<InMemoryBackend>,
38/// repo: Arc<UserRepository>,
39/// }
40///
41/// impl UserService {
42/// pub fn get(&self, id: &str) -> Result<Option<User>> {
43/// let mut feeder = UserFeeder { id: id.to_string(), user: None };
44/// self.cache.execute(&mut feeder, &*self.repo, CacheStrategy::Refresh)?;
45/// Ok(feeder.user)
46/// }
47/// }
48/// ```
49#[derive(Clone)]
50pub struct CacheService<B: CacheBackend> {
51 expander: Arc<CacheExpander<B>>,
52}
53
54impl<B: CacheBackend> CacheService<B> {
55 /// Create a new cache service with the given backend.
56 pub fn new(backend: B) -> Self {
57 CacheService {
58 expander: Arc::new(CacheExpander::new(backend)),
59 }
60 }
61
62 /// Create a new cache service with custom metrics.
63 pub fn with_metrics(backend: B, metrics: Box<dyn CacheMetrics>) -> Self {
64 CacheService {
65 expander: Arc::new(CacheExpander::new(backend).with_metrics(metrics)),
66 }
67 }
68
69 /// Execute a cache operation.
70 ///
71 /// This is equivalent to calling `expander.with()` but more ergonomic
72 /// for service-oriented architectures.
73 ///
74 /// # Arguments
75 ///
76 /// - `feeder`: Entity feeder (implements `CacheFeed<T>`)
77 /// - `repository`: Data repository (implements `DataRepository<T>`)
78 /// - `strategy`: Cache strategy (Fresh, Refresh, Invalidate, Bypass)
79 ///
80 /// # Example
81 ///
82 /// ```ignore
83 /// let cache = CacheService::new(InMemoryBackend::new());
84 /// let mut feeder = UserFeeder { id: "user_123".to_string(), user: None };
85 /// let repo = UserRepository::new(pool);
86 ///
87 /// cache.execute(&mut feeder, &repo, CacheStrategy::Refresh).await?;
88 /// let user = feeder.user;
89 /// ```
90 ///
91 /// # Errors
92 ///
93 /// Returns `Err` in these cases:
94 /// - `Error::ValidationError`: Feeder validation fails
95 /// - `Error::DeserializationError`: Cached data is corrupted
96 /// - `Error::InvalidCacheEntry`: Invalid cache envelope
97 /// - `Error::VersionMismatch`: Schema version mismatch
98 /// - `Error::BackendError`: Cache backend unavailable
99 /// - `Error::RepositoryError`: Database access fails
100 /// - `Error::SerializationError`: Entity serialization fails
101 pub async fn execute<T, F, R>(
102 &self,
103 feeder: &mut F,
104 repository: &R,
105 strategy: CacheStrategy,
106 ) -> Result<()>
107 where
108 T: CacheEntity,
109 F: CacheFeed<T>,
110 R: DataRepository<T>,
111 T::Key: FromStr,
112 {
113 self.expander
114 .with::<T, F, R>(feeder, repository, strategy)
115 .await
116 }
117
118 /// Execute a cache operation with advanced configuration.
119 ///
120 /// This method allows per-operation configuration such as TTL override and retry logic,
121 /// while working seamlessly with Arc-wrapped services.
122 ///
123 /// # Arguments
124 ///
125 /// - `feeder`: Entity feeder (implements `CacheFeed<T>`)
126 /// - `repository`: Data repository (implements `DataRepository<T>`)
127 /// - `strategy`: Cache strategy (Fresh, Refresh, Invalidate, Bypass)
128 /// - `config`: Operation configuration (TTL override, retry count)
129 ///
130 /// # Example
131 ///
132 /// ```ignore
133 /// let cache = CacheService::new(InMemoryBackend::new());
134 /// let mut feeder = UserFeeder { id: "user_123".to_string(), user: None };
135 /// let repo = UserRepository::new(pool);
136 ///
137 /// let config = OperationConfig::default()
138 /// .with_ttl(Duration::from_secs(300))
139 /// .with_retry(3);
140 ///
141 /// cache.execute_with_config(&mut feeder, &repo, CacheStrategy::Refresh, config).await?;
142 /// let user = feeder.user;
143 /// ```
144 ///
145 /// # Errors
146 ///
147 /// Same error cases as `execute()`, plus retry-related failures.
148 pub async fn execute_with_config<T, F, R>(
149 &self,
150 feeder: &mut F,
151 repository: &R,
152 strategy: CacheStrategy,
153 config: OperationConfig,
154 ) -> Result<()>
155 where
156 T: CacheEntity,
157 F: CacheFeed<T>,
158 R: DataRepository<T>,
159 T::Key: FromStr,
160 {
161 self.expander
162 .with_config::<T, F, R>(feeder, repository, strategy, config)
163 .await
164 }
165
166 /// Get a reference to the underlying expander.
167 ///
168 /// Use this if you need direct access to expander methods.
169 pub fn expander(&self) -> &CacheExpander<B> {
170 &self.expander
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::backend::InMemoryBackend;
178 use crate::feed::GenericFeeder;
179 use crate::repository::InMemoryRepository;
180 use serde::{Deserialize, Serialize};
181
182 #[derive(Clone, Serialize, Deserialize)]
183 struct TestEntity {
184 id: String,
185 value: String,
186 }
187
188 impl CacheEntity for TestEntity {
189 type Key = String;
190
191 fn cache_key(&self) -> Self::Key {
192 self.id.clone()
193 }
194
195 fn cache_prefix() -> &'static str {
196 "test"
197 }
198 }
199
200 #[test]
201 fn test_cache_service_creation() {
202 let backend = InMemoryBackend::new();
203 let _service = CacheService::new(backend);
204 }
205
206 #[tokio::test]
207 async fn test_cache_service_execute() {
208 let backend = InMemoryBackend::new();
209 let service = CacheService::new(backend);
210
211 let mut repo = InMemoryRepository::new();
212 repo.insert(
213 "1".to_string(),
214 TestEntity {
215 id: "1".to_string(),
216 value: "test_value".to_string(),
217 },
218 );
219
220 let mut feeder = GenericFeeder::new("1".to_string());
221
222 service
223 .execute::<TestEntity, _, _>(&mut feeder, &repo, CacheStrategy::Refresh)
224 .await
225 .expect("Failed to execute");
226
227 assert!(feeder.data.is_some());
228 assert_eq!(feeder.data.expect("Data not found").value, "test_value");
229 }
230
231 #[test]
232 fn test_cache_service_clone() {
233 let backend = InMemoryBackend::new();
234 let service1 = CacheService::new(backend);
235 let service2 = service1.clone();
236
237 // Both services share the same expander
238 assert!(Arc::ptr_eq(&service1.expander, &service2.expander));
239 }
240
241 #[test]
242 fn test_cache_service_expander_access() {
243 let backend = InMemoryBackend::new();
244 let service = CacheService::new(backend);
245
246 // Can access expander directly
247 let _expander = service.expander();
248 }
249
250 #[tokio::test]
251 async fn test_cache_service_thread_safety() {
252 let backend = InMemoryBackend::new();
253 let service = CacheService::new(backend);
254
255 let mut handles = vec![];
256
257 for i in 0..5 {
258 let service_clone = service.clone();
259 let handle = tokio::spawn(async move {
260 let mut repo = InMemoryRepository::new();
261 repo.insert(
262 format!("{}", i),
263 TestEntity {
264 id: format!("{}", i),
265 value: format!("value_{}", i),
266 },
267 );
268
269 let mut feeder = GenericFeeder::new(format!("{}", i));
270 service_clone
271 .execute::<TestEntity, _, _>(&mut feeder, &repo, CacheStrategy::Refresh)
272 .await
273 .expect("Failed to execute");
274
275 assert!(feeder.data.is_some());
276 });
277 handles.push(handle);
278 }
279
280 for handle in handles {
281 handle.await.expect("Task failed");
282 }
283 }
284
285 #[tokio::test]
286 async fn test_cache_service_execute_with_config() {
287 use std::time::Duration;
288
289 let backend = InMemoryBackend::new();
290 let service = CacheService::new(backend);
291
292 let mut repo = InMemoryRepository::new();
293 repo.insert(
294 "1".to_string(),
295 TestEntity {
296 id: "1".to_string(),
297 value: "test_value".to_string(),
298 },
299 );
300
301 let mut feeder = GenericFeeder::new("1".to_string());
302
303 // Test with custom config
304 let config = OperationConfig::default()
305 .with_ttl(Duration::from_secs(300))
306 .with_retry(3);
307
308 service
309 .execute_with_config::<TestEntity, _, _>(
310 &mut feeder,
311 &repo,
312 CacheStrategy::Refresh,
313 config,
314 )
315 .await
316 .expect("Failed to execute with config");
317
318 assert!(feeder.data.is_some());
319 assert_eq!(feeder.data.expect("Data not found").value, "test_value");
320 }
321}