reinhardt_views/viewsets/
cached.rs1use async_trait::async_trait;
7use reinhardt_http::{Request, Response, Result};
8use reinhardt_utils::cache::Cache;
9use serde::{Deserialize, Serialize};
10use std::collections::HashSet;
11use std::sync::Arc;
12use std::time::Duration;
13use tokio::sync::RwLock;
14
15#[derive(Debug, Clone)]
17pub struct CacheConfig {
18 pub key_prefix: String,
20 pub ttl: Option<Duration>,
22 pub cache_list: bool,
24 pub cache_retrieve: bool,
26}
27
28impl CacheConfig {
29 pub fn new(key_prefix: impl Into<String>) -> Self {
46 Self {
47 key_prefix: key_prefix.into(),
48 ttl: None,
49 cache_list: true,
50 cache_retrieve: true,
51 }
52 }
53
54 pub fn with_ttl(mut self, ttl: Duration) -> Self {
56 self.ttl = Some(ttl);
57 self
58 }
59
60 pub fn cache_list_only(mut self) -> Self {
62 self.cache_list = true;
63 self.cache_retrieve = false;
64 self
65 }
66
67 pub fn cache_retrieve_only(mut self) -> Self {
69 self.cache_list = false;
70 self.cache_retrieve = true;
71 self
72 }
73
74 pub fn cache_all(mut self) -> Self {
76 self.cache_list = true;
77 self.cache_retrieve = true;
78 self
79 }
80}
81
82impl Default for CacheConfig {
83 fn default() -> Self {
84 Self::new("viewset")
85 .with_ttl(Duration::from_secs(300)) .cache_all()
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct CachedResponse {
93 pub status: u16,
95 pub body: Vec<u8>,
97 pub headers: Vec<(String, String)>,
99}
100
101impl CachedResponse {
102 pub fn from_response(response: &Response) -> Self {
104 let headers = response
105 .headers
106 .iter()
107 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
108 .collect();
109
110 Self {
111 status: response.status.as_u16(),
112 body: response.body.to_vec(),
113 headers,
114 }
115 }
116
117 pub fn to_response(&self) -> Response {
119 use hyper::StatusCode;
120
121 let mut response =
122 Response::new(StatusCode::from_u16(self.status).unwrap_or(StatusCode::OK));
123
124 response.body = self.body.clone().into();
125
126 for (key, value) in &self.headers {
127 if let Ok(header_name) = hyper::header::HeaderName::from_bytes(key.as_bytes())
128 && let Ok(header_value) = hyper::header::HeaderValue::from_str(value)
129 {
130 response.headers.insert(header_name, header_value);
131 }
132 }
133
134 response
135 }
136}
137
138pub struct CachedViewSet<V, C> {
181 inner: Arc<V>,
183 cache: Arc<C>,
185 config: CacheConfig,
187 cache_tag: String,
189 cached_keys: Arc<RwLock<HashSet<String>>>,
191}
192
193impl<V, C> CachedViewSet<V, C>
194where
195 C: Cache,
196{
197 pub fn new(inner: V, cache: C, config: CacheConfig) -> Self {
199 let cache_tag = format!("viewset:{}", config.key_prefix);
200 Self {
201 inner: Arc::new(inner),
202 cache: Arc::new(cache),
203 config,
204 cache_tag,
205 cached_keys: Arc::new(RwLock::new(HashSet::new())),
206 }
207 }
208
209 pub fn cache_tag(&self) -> &str {
211 &self.cache_tag
212 }
213
214 fn list_cache_key(&self, query_string: &str) -> String {
216 format!("{}:list:{}", self.config.key_prefix, query_string)
217 }
218
219 fn retrieve_cache_key(&self, id: &str) -> String {
221 format!("{}:retrieve:{}", self.config.key_prefix, id)
222 }
223
224 pub fn inner(&self) -> Arc<V> {
226 self.inner.clone()
227 }
228
229 pub fn cache(&self) -> Arc<C> {
231 self.cache.clone()
232 }
233
234 pub async fn invalidate_all(&self) -> Result<()> {
239 let keys: Vec<String> = {
241 let mut cached_keys = self.cached_keys.write().await;
242 cached_keys.drain().collect()
243 };
244
245 for key in &keys {
247 let _ = self.cache.delete(key).await;
249 }
250
251 Ok(())
252 }
253
254 async fn track_cache_key(&self, key: &str) {
256 let mut cached_keys = self.cached_keys.write().await;
257 cached_keys.insert(key.to_string());
258 }
259
260 pub async fn invalidate_item(&self, id: &str) -> Result<()> {
262 let key = self.retrieve_cache_key(id);
263
264 {
266 let mut cached_keys = self.cached_keys.write().await;
267 cached_keys.remove(&key);
268 }
269
270 self.cache.delete(&key).await?;
271 Ok(())
272 }
273}
274
275#[async_trait]
277pub trait CachedViewSetTrait: Send + Sync {
278 async fn cached_list(&self, request: Request) -> Result<Response>;
280
281 async fn cached_retrieve(&self, request: Request, id: String) -> Result<Response>;
283
284 async fn invalidate(&self, id: &str) -> Result<()>;
286
287 async fn invalidate_all(&self) -> Result<()>;
289}
290
291#[async_trait]
292impl<V, C> CachedViewSetTrait for CachedViewSet<V, C>
293where
294 V: crate::viewsets::ListMixin + crate::viewsets::RetrieveMixin + Send + Sync + 'static,
295 C: Cache + Send + Sync + 'static,
296{
297 async fn cached_list(&self, request: Request) -> Result<Response> {
298 if !self.config.cache_list {
299 return self.inner.list(request).await;
301 }
302
303 let query_string = request.uri.query().unwrap_or("");
304 let cache_key = self.list_cache_key(query_string);
305
306 if let Some(cached) = self.cache.get::<CachedResponse>(&cache_key).await? {
308 return Ok(cached.to_response());
309 }
310
311 let response = self.inner.list(request).await?;
313 let cached = CachedResponse::from_response(&response);
314
315 self.cache.set(&cache_key, &cached, self.config.ttl).await?;
317 self.track_cache_key(&cache_key).await;
318
319 Ok(response)
320 }
321
322 async fn cached_retrieve(&self, request: Request, id: String) -> Result<Response> {
323 if !self.config.cache_retrieve {
324 return self.inner.retrieve(request, id).await;
326 }
327
328 let cache_key = self.retrieve_cache_key(&id);
329
330 if let Some(cached) = self.cache.get::<CachedResponse>(&cache_key).await? {
332 return Ok(cached.to_response());
333 }
334
335 let response = self.inner.retrieve(request, id.clone()).await?;
337 let cached = CachedResponse::from_response(&response);
338
339 self.cache.set(&cache_key, &cached, self.config.ttl).await?;
341 self.track_cache_key(&cache_key).await;
342
343 Ok(response)
344 }
345
346 async fn invalidate(&self, id: &str) -> Result<()> {
347 self.invalidate_item(id).await
348 }
349
350 async fn invalidate_all(&self) -> Result<()> {
351 self.invalidate_all().await
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use bytes::Bytes;
359 use hyper::StatusCode;
360 use reinhardt_utils::cache::InMemoryCache;
361
362 #[test]
363 fn test_cache_config_builder() {
364 let config = CacheConfig::new("users")
365 .with_ttl(Duration::from_secs(300))
366 .cache_all();
367
368 assert_eq!(config.key_prefix, "users");
369 assert_eq!(config.ttl, Some(Duration::from_secs(300)));
370 assert!(config.cache_list);
371 assert!(config.cache_retrieve);
372 }
373
374 #[test]
375 fn test_cache_config_list_only() {
376 let config = CacheConfig::new("posts").cache_list_only();
377
378 assert!(config.cache_list);
379 assert!(!config.cache_retrieve);
380 }
381
382 #[test]
383 fn test_cache_config_retrieve_only() {
384 let config = CacheConfig::new("posts").cache_retrieve_only();
385
386 assert!(!config.cache_list);
387 assert!(config.cache_retrieve);
388 }
389
390 #[test]
391 fn test_cached_response_conversion() {
392 let mut original = Response::new(StatusCode::OK);
393 original.body = Bytes::from("test body");
394 let cached = CachedResponse::from_response(&original);
395
396 assert_eq!(cached.status, 200);
397 assert_eq!(cached.body, b"test body");
398
399 let restored = cached.to_response();
400 assert_eq!(restored.status, StatusCode::OK);
401 assert_eq!(restored.body, Bytes::from("test body"));
402 }
403
404 #[test]
405 fn test_cached_viewset_creation() {
406 #[derive(Debug, Clone)]
407 struct TestViewSet {
408 #[allow(dead_code)]
410 name: String,
411 }
412
413 let inner = TestViewSet {
414 name: "users".to_string(),
415 };
416 let cache = InMemoryCache::new();
417 let config = CacheConfig::new("users").cache_all();
418
419 let cached_viewset = CachedViewSet::new(inner, cache, config);
420 assert_eq!(cached_viewset.config.key_prefix, "users");
421 }
422
423 #[test]
424 fn test_cache_keys() {
425 #[derive(Debug, Clone)]
426 struct TestViewSet;
427
428 let inner = TestViewSet;
429 let cache = InMemoryCache::new();
430 let config = CacheConfig::new("users");
431
432 let cached_viewset = CachedViewSet::new(inner, cache, config);
433
434 let list_key = cached_viewset.list_cache_key("page=1&limit=10");
435 assert_eq!(list_key, "users:list:page=1&limit=10");
436
437 let retrieve_key = cached_viewset.retrieve_cache_key("123");
438 assert_eq!(retrieve_key, "users:retrieve:123");
439 }
440
441 #[tokio::test]
442 async fn test_invalidate_item() {
443 #[derive(Debug, Clone)]
444 struct TestViewSet;
445
446 let inner = TestViewSet;
447 let cache = InMemoryCache::new();
448 let config = CacheConfig::new("users");
449
450 let cached_viewset = CachedViewSet::new(inner, cache.clone(), config);
451
452 let cached_response = CachedResponse {
454 status: 200,
455 body: b"cached data".to_vec(),
456 headers: vec![],
457 };
458 cache
459 .set("users:retrieve:123", &cached_response, None)
460 .await
461 .unwrap();
462
463 let cached: Option<CachedResponse> = cache.get("users:retrieve:123").await.unwrap();
465 assert!(cached.is_some());
466
467 cached_viewset.invalidate_item("123").await.unwrap();
469
470 let cached: Option<CachedResponse> = cache.get("users:retrieve:123").await.unwrap();
472 assert!(cached.is_none());
473 }
474
475 #[tokio::test]
476 async fn test_invalidate_all() {
477 #[derive(Debug, Clone)]
478 struct TestViewSet;
479
480 let inner = TestViewSet;
481 let cache = InMemoryCache::new();
482 let config = CacheConfig::new("users");
483
484 let cached_viewset = CachedViewSet::new(inner, cache.clone(), config);
485
486 let cached_response = CachedResponse {
488 status: 200,
489 body: b"cached data".to_vec(),
490 headers: vec![],
491 };
492
493 cache
495 .set("users:retrieve:123", &cached_response, None)
496 .await
497 .unwrap();
498 cached_viewset.track_cache_key("users:retrieve:123").await;
499
500 cache
501 .set("users:list:page=1", &cached_response, None)
502 .await
503 .unwrap();
504 cached_viewset.track_cache_key("users:list:page=1").await;
505
506 let cached1: Option<CachedResponse> = cache.get("users:retrieve:123").await.unwrap();
508 let cached2: Option<CachedResponse> = cache.get("users:list:page=1").await.unwrap();
509 assert!(cached1.is_some());
510 assert!(cached2.is_some());
511
512 cached_viewset.invalidate_all().await.unwrap();
514
515 let cached1: Option<CachedResponse> = cache.get("users:retrieve:123").await.unwrap();
517 let cached2: Option<CachedResponse> = cache.get("users:list:page=1").await.unwrap();
518 assert!(cached1.is_none());
519 assert!(cached2.is_none());
520 }
521
522 #[tokio::test]
523 async fn test_invalidate_all_does_not_affect_other_viewsets() {
524 #[derive(Debug, Clone)]
525 struct TestViewSet;
526
527 let cache = InMemoryCache::new();
528
529 let users_viewset =
531 CachedViewSet::new(TestViewSet, cache.clone(), CacheConfig::new("users"));
532 let posts_viewset =
533 CachedViewSet::new(TestViewSet, cache.clone(), CacheConfig::new("posts"));
534
535 let cached_response = CachedResponse {
536 status: 200,
537 body: b"cached data".to_vec(),
538 headers: vec![],
539 };
540
541 cache
543 .set("users:retrieve:1", &cached_response, None)
544 .await
545 .unwrap();
546 users_viewset.track_cache_key("users:retrieve:1").await;
547
548 cache
549 .set("posts:retrieve:1", &cached_response, None)
550 .await
551 .unwrap();
552 posts_viewset.track_cache_key("posts:retrieve:1").await;
553
554 users_viewset.invalidate_all().await.unwrap();
556
557 let users_cached: Option<CachedResponse> = cache.get("users:retrieve:1").await.unwrap();
559 assert!(users_cached.is_none());
560
561 let posts_cached: Option<CachedResponse> = cache.get("posts:retrieve:1").await.unwrap();
563 assert!(posts_cached.is_some());
564 }
565
566 #[test]
567 fn test_cache_config_default() {
568 let config = CacheConfig::default();
569 assert_eq!(config.key_prefix, "viewset");
570 assert!(config.cache_list);
571 assert!(config.cache_retrieve);
572 assert_eq!(config.ttl, Some(Duration::from_secs(300))); }
574}