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}