cache_kit/repository.rs
1//! Data repository trait for abstracting database access.
2//!
3//! The `DataRepository` trait decouples cache-kit from specific database implementations.
4//! This allows you to plug in any storage backend and makes testing your cache-using code
5//! straightforward via mockable implementations.
6//!
7//! # Implementing DataRepository
8//!
9//! Implement this trait for any storage backend:
10//! - SQL databases: SQLx, tokio-postgres, Diesel
11//! - NoSQL: MongoDB, DynamoDB, Firestore
12//! - In-memory: For testing (provided in this module)
13//! - Custom ORMs or proprietary systems
14//!
15//! # Mocking for Tests
16//!
17//! Create simple in-memory implementations for unit testing:
18//!
19//! ```ignore
20//! use cache_kit::repository::InMemoryRepository;
21//! use cache_kit::entity::CacheEntity;
22//! use serde::{Deserialize, Serialize};
23//!
24//! #[derive(Clone, Serialize, Deserialize)]
25//! pub struct User {
26//! id: String,
27//! name: String,
28//! }
29//!
30//! impl CacheEntity for User {
31//! type Key = String;
32//! fn cache_key(&self) -> Self::Key { self.id.clone() }
33//! fn cache_prefix() -> &'static str { "user" }
34//! }
35//!
36//! #[tokio::test]
37//! async fn test_cache_with_mock_repo() {
38//! let mut repo = InMemoryRepository::new();
39//! repo.insert("user:1".to_string(), User {
40//! id: "user:1".to_string(),
41//! name: "Alice".to_string(),
42//! });
43//!
44//! // Use repo in your cache-kit code
45//! let user = repo.fetch_by_id(&"user:1".to_string()).await.unwrap();
46//! assert_eq!(user.map(|u| u.name), Some("Alice".to_string()));
47//! }
48//! ```
49//!
50//! # Error Handling
51//!
52//! When implementing the trait for real databases, return `Err` for:
53//! - Database connectivity issues
54//! - Query timeouts
55//! - Authentication failures
56//! - Serialization errors
57//! - Any other storage operation failures
58
59use crate::entity::CacheEntity;
60use crate::error::Result;
61
62/// Trait for data repository implementations.
63///
64/// Abstracts database operations, decoupling cache from specific DB client.
65/// Implementations: SQLx, tokio-postgres, Diesel, custom ORM, in-memory, etc.
66///
67/// # Design for Testability
68///
69/// This trait is designed to be mockable. Implement it with your database client,
70/// or use `InMemoryRepository` provided in this module for testing.
71#[allow(async_fn_in_trait)]
72pub trait DataRepository<T: CacheEntity>: Send + Sync {
73 /// Fetch entity by ID from primary data source.
74 ///
75 /// Called when cache miss occurs or on explicit refresh.
76 ///
77 /// # Returns
78 /// - `Ok(Some(entity))` - Entity found
79 /// - `Ok(None)` - Entity not found (not an error)
80 /// - `Err(e)` - Database error
81 ///
82 /// # Errors
83 /// Returns `Err` if data source is unavailable or fetch fails
84 async fn fetch_by_id(&self, id: &T::Key) -> Result<Option<T>>;
85
86 /// Batch fetch entities by IDs (optional optimization).
87 ///
88 /// Default implementation calls `fetch_by_id()` for each key.
89 /// Override for efficiency (e.g., SQL `WHERE id IN (...)`)
90 ///
91 /// # Errors
92 /// Returns `Err` if data source is unavailable or fetch fails
93 async fn fetch_by_ids(&self, ids: &[T::Key]) -> Result<Vec<Option<T>>> {
94 let mut results = Vec::with_capacity(ids.len());
95 for id in ids {
96 results.push(self.fetch_by_id(id).await?);
97 }
98 Ok(results)
99 }
100
101 /// Count total entities (optional, for statistics).
102 ///
103 /// # Errors
104 /// Returns `Err` if not implemented or if data source operation fails
105 async fn count(&self) -> Result<u64> {
106 Err(crate::error::Error::NotImplemented(
107 "count not implemented".to_string(),
108 ))
109 }
110
111 /// Optional: Get all entities (use sparingly, potentially large result).
112 ///
113 /// # Errors
114 /// Returns `Err` if not implemented or if data source operation fails
115 async fn fetch_all(&self) -> Result<Vec<T>> {
116 Err(crate::error::Error::NotImplemented(
117 "fetch_all not implemented for this repository".to_string(),
118 ))
119 }
120}
121
122// ============================================================================
123// In-Memory Test Repository
124// ============================================================================
125
126use std::collections::HashMap;
127
128/// Simple in-memory repository for testing cache-kit implementations.
129///
130/// Provides a straightforward mock `DataRepository` suitable for unit tests
131/// where you want to control what data is "stored" without setting up a real database.
132///
133/// # Why Use InMemoryRepository
134///
135/// - **Fast Tests**: No database setup, teardown, or network calls
136/// - **Deterministic**: Control exactly what data is present
137/// - **Isolated**: Each test can have its own data without conflicts
138/// - **Simple**: Easy to understand test behavior and debug failures
139///
140/// # Example Usage
141///
142/// ```ignore
143/// #[tokio::test]
144/// async fn test_cache_expander_with_mock_data() {
145/// // Create and populate mock repository
146/// let mut repo = InMemoryRepository::new();
147/// repo.insert("user:1".to_string(), my_user_entity);
148///
149/// // Use with cache-kit components
150/// let mut feeder = MyFeeder::new();
151/// let result = expander.with(&mut feeder, &repo, CacheStrategy::Refresh).await;
152///
153/// assert!(result.is_ok());
154/// }
155/// ```
156///
157/// # Testing Different Scenarios
158///
159/// - **Cache hit**: Populate repo, cache will find the data
160/// - **Cache miss**: Keep repo empty, cache will fallback to repo (which has nothing)
161/// - **Invalidation**: Clear repo between operations to test refresh behavior
162/// - **Batch operations**: Use `fetch_by_ids()` to test multi-key scenarios
163pub struct InMemoryRepository<T: CacheEntity> {
164 data: HashMap<String, T>,
165}
166
167impl<T: CacheEntity> InMemoryRepository<T> {
168 /// Create a new empty in-memory repository.
169 ///
170 /// # Example
171 ///
172 /// ```ignore
173 /// let repo = InMemoryRepository::<MyEntity>::new();
174 /// assert!(repo.is_empty());
175 /// ```
176 pub fn new() -> Self {
177 InMemoryRepository {
178 data: HashMap::new(),
179 }
180 }
181
182 /// Insert or update an entity by key.
183 ///
184 /// # Example
185 ///
186 /// ```ignore
187 /// repo.insert("user:123".to_string(), my_user);
188 /// let found = repo.fetch_by_id(&"user:123".to_string()).await?;
189 /// ```
190 pub fn insert(&mut self, id: T::Key, value: T) {
191 self.data.insert(id.to_string(), value);
192 }
193
194 /// Remove all entities from the repository.
195 ///
196 /// Useful for resetting state between test cases.
197 ///
198 /// # Example
199 ///
200 /// ```ignore
201 /// repo.insert("user:1".to_string(), entity);
202 /// assert_eq!(repo.len(), 1);
203 /// repo.clear();
204 /// assert!(repo.is_empty());
205 /// ```
206 pub fn clear(&mut self) {
207 self.data.clear();
208 }
209
210 /// Return the number of entities in the repository.
211 pub fn len(&self) -> usize {
212 self.data.len()
213 }
214
215 /// Return true if the repository contains no entities.
216 pub fn is_empty(&self) -> bool {
217 self.data.is_empty()
218 }
219}
220
221impl<T: CacheEntity> Default for InMemoryRepository<T> {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227impl<T: CacheEntity> DataRepository<T> for InMemoryRepository<T> {
228 async fn fetch_by_id(&self, id: &T::Key) -> Result<Option<T>> {
229 Ok(self.data.get(&id.to_string()).cloned())
230 }
231
232 async fn fetch_by_ids(&self, ids: &[T::Key]) -> Result<Vec<Option<T>>> {
233 Ok(ids
234 .iter()
235 .map(|id| self.data.get(&id.to_string()).cloned())
236 .collect())
237 }
238
239 async fn count(&self) -> Result<u64> {
240 Ok(self.data.len() as u64)
241 }
242
243 async fn fetch_all(&self) -> Result<Vec<T>> {
244 Ok(self.data.values().cloned().collect())
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use serde::{Deserialize, Serialize};
252
253 #[derive(Clone, Serialize, Deserialize)]
254 struct TestEntity {
255 id: String,
256 value: String,
257 }
258
259 impl CacheEntity for TestEntity {
260 type Key = String;
261
262 fn cache_key(&self) -> Self::Key {
263 self.id.clone()
264 }
265
266 fn cache_prefix() -> &'static str {
267 "test"
268 }
269 }
270
271 #[tokio::test]
272 async fn test_in_memory_repository() {
273 let mut repo = InMemoryRepository::new();
274
275 let entity = TestEntity {
276 id: "1".to_string(),
277 value: "data".to_string(),
278 };
279
280 repo.insert("1".to_string(), entity.clone());
281
282 let fetched = repo
283 .fetch_by_id(&"1".to_string())
284 .await
285 .expect("Failed to fetch");
286 assert!(fetched.is_some());
287 assert_eq!(fetched.expect("Entity not found").value, "data");
288 }
289
290 #[tokio::test]
291 async fn test_in_memory_repository_miss() {
292 let repo: InMemoryRepository<TestEntity> = InMemoryRepository::new();
293
294 let fetched = repo
295 .fetch_by_id(&"nonexistent".to_string())
296 .await
297 .expect("Failed to fetch");
298 assert!(fetched.is_none());
299 }
300
301 #[tokio::test]
302 async fn test_in_memory_repository_batch() {
303 let mut repo = InMemoryRepository::new();
304
305 repo.insert(
306 "1".to_string(),
307 TestEntity {
308 id: "1".to_string(),
309 value: "a".to_string(),
310 },
311 );
312
313 repo.insert(
314 "2".to_string(),
315 TestEntity {
316 id: "2".to_string(),
317 value: "b".to_string(),
318 },
319 );
320
321 let ids = vec!["1".to_string(), "2".to_string(), "3".to_string()];
322 let results = repo
323 .fetch_by_ids(&ids)
324 .await
325 .expect("Failed to fetch batch");
326
327 assert_eq!(results.len(), 3);
328 assert!(results[0].is_some());
329 assert!(results[1].is_some());
330 assert!(results[2].is_none());
331 }
332
333 #[tokio::test]
334 async fn test_in_memory_repository_count() {
335 let mut repo = InMemoryRepository::new();
336
337 repo.insert(
338 "1".to_string(),
339 TestEntity {
340 id: "1".to_string(),
341 value: "a".to_string(),
342 },
343 );
344
345 assert_eq!(repo.count().await.expect("Failed to count"), 1);
346 }
347}