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}