Skip to main content

reinhardt_views/viewsets/
cached.rs

1//! Response caching support for ViewSets
2//!
3//! Provides automatic caching for read-only operations (list and retrieve).
4//! Supports TTL-based expiration and cache invalidation.
5
6use 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/// Cache configuration for ViewSets
16#[derive(Debug, Clone)]
17pub struct CacheConfig {
18	/// Cache key prefix
19	pub key_prefix: String,
20	/// Time-to-live for cached responses
21	pub ttl: Option<Duration>,
22	/// Whether to cache list() responses
23	pub cache_list: bool,
24	/// Whether to cache retrieve() responses
25	pub cache_retrieve: bool,
26}
27
28impl CacheConfig {
29	/// Create a new cache configuration
30	///
31	/// # Examples
32	///
33	/// ```
34	/// use reinhardt_views::viewsets::CacheConfig;
35	/// use std::time::Duration;
36	///
37	/// let config = CacheConfig::new("users")
38	///     .with_ttl(Duration::from_secs(300))
39	///     .cache_all();
40	///
41	/// assert_eq!(config.key_prefix, "users");
42	/// assert!(config.cache_list);
43	/// assert!(config.cache_retrieve);
44	/// ```
45	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	/// Set TTL for cached responses
55	pub fn with_ttl(mut self, ttl: Duration) -> Self {
56		self.ttl = Some(ttl);
57		self
58	}
59
60	/// Enable caching for list() operations
61	pub fn cache_list_only(mut self) -> Self {
62		self.cache_list = true;
63		self.cache_retrieve = false;
64		self
65	}
66
67	/// Enable caching for retrieve() operations
68	pub fn cache_retrieve_only(mut self) -> Self {
69		self.cache_list = false;
70		self.cache_retrieve = true;
71		self
72	}
73
74	/// Enable caching for all read operations
75	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)) // 5 minutes default TTL
86			.cache_all()
87	}
88}
89
90/// Cached response wrapper
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct CachedResponse {
93	/// HTTP status code
94	pub status: u16,
95	/// Response body
96	pub body: Vec<u8>,
97	/// Response headers (simplified)
98	pub headers: Vec<(String, String)>,
99}
100
101impl CachedResponse {
102	/// Create from a Response
103	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	/// Convert to Response
118	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
138/// Cached ViewSet wrapper
139///
140/// # Example
141///
142/// ```
143/// use reinhardt_views::viewsets::{CachedViewSet, CacheConfig, ModelViewSet};
144/// use reinhardt_rest::serializers::JsonSerializer;
145/// use reinhardt_utils::cache::InMemoryCache;
146/// use reinhardt_db::orm::{FieldSelector, Model};
147/// use std::time::Duration;
148///
149/// #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
150/// struct User {
151///     id: Option<i64>,
152///     name: String,
153/// }
154///
155/// #[derive(Clone)]
156/// struct UserFields;
157/// impl FieldSelector for UserFields { fn with_alias(self, _: &str) -> Self { self } }
158/// impl Model for User {
159///     type PrimaryKey = i64;
160///     type Fields = UserFields;
161///     type Objects = reinhardt_db::orm::Manager<Self>;
162///     fn table_name() -> &'static str { "users" }
163///     fn primary_key(&self) -> Option<i64> { self.id }
164///     fn set_primary_key(&mut self, v: i64) { self.id = Some(v); }
165///     fn new_fields() -> Self::Fields { UserFields }
166/// }
167///
168/// type UserSerializer = JsonSerializer<User>;
169///
170/// # async fn example() {
171/// let cache = InMemoryCache::new();
172/// let inner_viewset = ModelViewSet::<User, UserSerializer>::new("users");
173/// let config = CacheConfig::new("users")
174///     .with_ttl(Duration::from_secs(300))
175///     .cache_all();
176///
177/// let cached_viewset = CachedViewSet::new(inner_viewset, cache, config);
178/// # }
179/// ```
180pub struct CachedViewSet<V, C> {
181	/// Inner ViewSet
182	inner: Arc<V>,
183	/// Cache backend
184	cache: Arc<C>,
185	/// Cache configuration
186	config: CacheConfig,
187	/// Tag for cache invalidation (format: "viewset:{key_prefix}")
188	cache_tag: String,
189	/// Tracked cache keys for selective invalidation
190	cached_keys: Arc<RwLock<HashSet<String>>>,
191}
192
193impl<V, C> CachedViewSet<V, C>
194where
195	C: Cache,
196{
197	/// Create a new cached ViewSet
198	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	/// Get the cache tag for this ViewSet
210	pub fn cache_tag(&self) -> &str {
211		&self.cache_tag
212	}
213
214	/// Get the cache key for a list operation
215	fn list_cache_key(&self, query_string: &str) -> String {
216		format!("{}:list:{}", self.config.key_prefix, query_string)
217	}
218
219	/// Get the cache key for a retrieve operation
220	fn retrieve_cache_key(&self, id: &str) -> String {
221		format!("{}:retrieve:{}", self.config.key_prefix, id)
222	}
223
224	/// Get the inner ViewSet
225	pub fn inner(&self) -> Arc<V> {
226		self.inner.clone()
227	}
228
229	/// Get the cache backend
230	pub fn cache(&self) -> Arc<C> {
231		self.cache.clone()
232	}
233
234	/// Invalidate all cached responses for this ViewSet
235	///
236	/// This method only invalidates cache entries created by this ViewSet,
237	/// not the entire cache. It uses tracked cache keys for selective invalidation.
238	pub async fn invalidate_all(&self) -> Result<()> {
239		// Get and clear the tracked keys
240		let keys: Vec<String> = {
241			let mut cached_keys = self.cached_keys.write().await;
242			cached_keys.drain().collect()
243		};
244
245		// Delete all tracked keys from the cache
246		for key in &keys {
247			// Ignore errors for individual key deletions (key may have expired)
248			let _ = self.cache.delete(key).await;
249		}
250
251		Ok(())
252	}
253
254	/// Track a cache key for later invalidation
255	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	/// Invalidate cached response for a specific item
261	pub async fn invalidate_item(&self, id: &str) -> Result<()> {
262		let key = self.retrieve_cache_key(id);
263
264		// Remove from tracked keys
265		{
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/// Trait for cached read operations
276#[async_trait]
277pub trait CachedViewSetTrait: Send + Sync {
278	/// Cached list operation
279	async fn cached_list(&self, request: Request) -> Result<Response>;
280
281	/// Cached retrieve operation
282	async fn cached_retrieve(&self, request: Request, id: String) -> Result<Response>;
283
284	/// Invalidate cache for a specific item
285	async fn invalidate(&self, id: &str) -> Result<()>;
286
287	/// Invalidate all cached items
288	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			// Caching disabled, passthrough to inner viewset
300			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		// Try to get from cache
307		if let Some(cached) = self.cache.get::<CachedResponse>(&cache_key).await? {
308			return Ok(cached.to_response());
309		}
310
311		// Cache miss - call inner viewset and cache result
312		let response = self.inner.list(request).await?;
313		let cached = CachedResponse::from_response(&response);
314
315		// Cache the response with configured TTL and track the key
316		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			// Caching disabled, passthrough to inner viewset
325			return self.inner.retrieve(request, id).await;
326		}
327
328		let cache_key = self.retrieve_cache_key(&id);
329
330		// Try to get from cache
331		if let Some(cached) = self.cache.get::<CachedResponse>(&cache_key).await? {
332			return Ok(cached.to_response());
333		}
334
335		// Cache miss - call inner viewset and cache result
336		let response = self.inner.retrieve(request, id.clone()).await?;
337		let cached = CachedResponse::from_response(&response);
338
339		// Cache the response with configured TTL and track the key
340		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: field set during construction to simulate a named viewset in test
409			#[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		// Set a cached value
453		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		// Verify it exists
464		let cached: Option<CachedResponse> = cache.get("users:retrieve:123").await.unwrap();
465		assert!(cached.is_some());
466
467		// Invalidate
468		cached_viewset.invalidate_item("123").await.unwrap();
469
470		// Verify it's gone
471		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		// Set multiple cached values and track them
487		let cached_response = CachedResponse {
488			status: 200,
489			body: b"cached data".to_vec(),
490			headers: vec![],
491		};
492
493		// Set and track cache keys (simulating what cached_list/cached_retrieve do)
494		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		// Verify they exist
507		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		// Invalidate all
513		cached_viewset.invalidate_all().await.unwrap();
514
515		// Verify all are gone
516		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		// Create two ViewSets with different prefixes
530		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		// Set and track cache keys for both viewsets
542		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		// Invalidate only users viewset
555		users_viewset.invalidate_all().await.unwrap();
556
557		// Users cache should be gone
558		let users_cached: Option<CachedResponse> = cache.get("users:retrieve:1").await.unwrap();
559		assert!(users_cached.is_none());
560
561		// Posts cache should still exist
562		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))); // 5 minutes default TTL
573	}
574}