cache_kit/
feed.rs

1//! Feeder trait for consuming cached data.
2
3use crate::entity::CacheEntity;
4use crate::error::Result;
5
6/// Generic trait for consuming cached data from operations.
7///
8/// Replaces specific feeder traits with a single generic abstraction.
9///
10/// # Example
11///
12/// ```no_run
13/// use cache_kit::{CacheFeed, CacheEntity};
14/// use serde::{Deserialize, Serialize};
15///
16/// #[derive(Clone, Serialize, Deserialize)]
17/// struct Employment {
18///     id: String,
19///     name: String,
20/// }
21///
22/// impl CacheEntity for Employment {
23///     type Key = String;
24///     fn cache_key(&self) -> Self::Key { self.id.clone() }
25///     fn cache_prefix() -> &'static str { "employment" }
26/// }
27///
28/// struct EmploymentFeeder {
29///     id: String,
30///     employment: Option<Employment>,
31/// }
32///
33/// impl CacheFeed<Employment> for EmploymentFeeder {
34///     fn entity_id(&mut self) -> String {
35///         self.id.clone()
36///     }
37///
38///     fn feed(&mut self, entity: Option<Employment>) {
39///         self.employment = entity;
40///     }
41/// }
42/// ```
43pub trait CacheFeed<T: CacheEntity>: Send {
44    /// Return the entity ID to fetch cache for.
45    ///
46    /// Called first by expander to determine which cache entry to fetch.
47    fn entity_id(&mut self) -> T::Key;
48
49    /// Feed the loaded entity into this feeder.
50    ///
51    /// Called by expander after successfully loading from cache.
52    /// The feeder stores the entity internally for later use.
53    fn feed(&mut self, entity: Option<T>);
54
55    /// Optional: Validate the feeder before processing.
56    ///
57    /// Called before attempting cache fetch. Use to validate state.
58    /// Example: Check that entity_id is not empty
59    fn validate(&self) -> Result<()> {
60        Ok(())
61    }
62
63    /// Optional: Called after entity is loaded but before returning.
64    ///
65    /// Useful for post-processing, logging, or metrics.
66    fn on_loaded(&mut self, _entity: &T) -> Result<()> {
67        Ok(())
68    }
69
70    /// Optional: Called when cache miss occurs.
71    ///
72    /// Useful for metrics or custom behavior.
73    fn on_miss(&mut self, _key: &str) -> Result<()> {
74        Ok(())
75    }
76
77    /// Optional: Called when cache hit occurs.
78    ///
79    /// Useful for metrics or logging.
80    fn on_hit(&mut self, _key: &str) -> Result<()> {
81        Ok(())
82    }
83}
84
85// ============================================================================
86// Generic Feeder Implementations
87// ============================================================================
88
89/// Generic feeder for single entities.
90pub struct GenericFeeder<T: CacheEntity> {
91    pub id: T::Key,
92    pub data: Option<T>,
93}
94
95impl<T: CacheEntity> GenericFeeder<T> {
96    pub fn new(id: T::Key) -> Self {
97        GenericFeeder { id, data: None }
98    }
99}
100
101impl<T: CacheEntity> CacheFeed<T> for GenericFeeder<T> {
102    fn entity_id(&mut self) -> T::Key {
103        self.id.clone()
104    }
105
106    fn feed(&mut self, entity: Option<T>) {
107        self.data = entity;
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use serde::{Deserialize, Serialize};
115
116    #[derive(Clone, Serialize, Deserialize)]
117    struct TestEntity {
118        id: String,
119        value: String,
120    }
121
122    impl CacheEntity for TestEntity {
123        type Key = String;
124
125        fn cache_key(&self) -> Self::Key {
126            self.id.clone()
127        }
128
129        fn cache_prefix() -> &'static str {
130            "test"
131        }
132    }
133
134    #[test]
135    fn test_generic_feeder() {
136        let mut feeder = GenericFeeder::new("test_id".to_string());
137
138        assert_eq!(feeder.entity_id(), "test_id");
139
140        let entity = TestEntity {
141            id: "test_id".to_string(),
142            value: "data".to_string(),
143        };
144
145        feeder.feed(Some(entity.clone()));
146        assert!(feeder.data.is_some());
147    }
148
149    #[test]
150    fn test_feeder_validation() {
151        let feeder: GenericFeeder<TestEntity> = GenericFeeder::new("id".to_string());
152        assert!(feeder.validate().is_ok());
153    }
154
155    #[test]
156    fn test_feeder_on_loaded_hook() {
157        use std::sync::{Arc, Mutex};
158
159        #[derive(Clone)]
160        struct TrackingFeeder {
161            id: String,
162            data: Option<TestEntity>,
163            loaded_count: Arc<Mutex<usize>>,
164        }
165
166        impl CacheFeed<TestEntity> for TrackingFeeder {
167            fn entity_id(&mut self) -> String {
168                self.id.clone()
169            }
170
171            fn feed(&mut self, entity: Option<TestEntity>) {
172                self.data = entity;
173            }
174
175            fn on_loaded(&mut self, _entity: &TestEntity) -> Result<()> {
176                *self.loaded_count.lock().unwrap() += 1;
177                Ok(())
178            }
179        }
180
181        let loaded_count = Arc::new(Mutex::new(0));
182        let mut feeder = TrackingFeeder {
183            id: "1".to_string(),
184            data: None,
185            loaded_count: loaded_count.clone(),
186        };
187
188        let entity = TestEntity {
189            id: "1".to_string(),
190            value: "test".to_string(),
191        };
192
193        feeder.on_loaded(&entity).unwrap();
194        assert_eq!(*loaded_count.lock().unwrap(), 1);
195    }
196
197    #[test]
198    fn test_feeder_on_hit_hook() {
199        use std::sync::{Arc, Mutex};
200
201        #[derive(Clone)]
202        struct HitTrackingFeeder {
203            id: String,
204            data: Option<TestEntity>,
205            hit_keys: Arc<Mutex<Vec<String>>>,
206        }
207
208        impl CacheFeed<TestEntity> for HitTrackingFeeder {
209            fn entity_id(&mut self) -> String {
210                self.id.clone()
211            }
212
213            fn feed(&mut self, entity: Option<TestEntity>) {
214                self.data = entity;
215            }
216
217            fn on_hit(&mut self, key: &str) -> Result<()> {
218                self.hit_keys.lock().unwrap().push(key.to_string());
219                Ok(())
220            }
221        }
222
223        let hit_keys = Arc::new(Mutex::new(Vec::new()));
224        let mut feeder = HitTrackingFeeder {
225            id: "1".to_string(),
226            data: None,
227            hit_keys: hit_keys.clone(),
228        };
229
230        feeder.on_hit("test:1").unwrap();
231        assert_eq!(hit_keys.lock().unwrap().len(), 1);
232        assert_eq!(hit_keys.lock().unwrap()[0], "test:1");
233    }
234
235    #[test]
236    fn test_feeder_on_miss_hook() {
237        use std::sync::{Arc, Mutex};
238
239        #[derive(Clone)]
240        struct MissTrackingFeeder {
241            id: String,
242            data: Option<TestEntity>,
243            miss_keys: Arc<Mutex<Vec<String>>>,
244        }
245
246        impl CacheFeed<TestEntity> for MissTrackingFeeder {
247            fn entity_id(&mut self) -> String {
248                self.id.clone()
249            }
250
251            fn feed(&mut self, entity: Option<TestEntity>) {
252                self.data = entity;
253            }
254
255            fn on_miss(&mut self, key: &str) -> Result<()> {
256                self.miss_keys.lock().unwrap().push(key.to_string());
257                Ok(())
258            }
259        }
260
261        let miss_keys = Arc::new(Mutex::new(Vec::new()));
262        let mut feeder = MissTrackingFeeder {
263            id: "1".to_string(),
264            data: None,
265            miss_keys: miss_keys.clone(),
266        };
267
268        feeder.on_miss("test:1").unwrap();
269        assert_eq!(miss_keys.lock().unwrap().len(), 1);
270        assert_eq!(miss_keys.lock().unwrap()[0], "test:1");
271    }
272
273    #[test]
274    fn test_generic_feeder_feed_none() {
275        let mut feeder: GenericFeeder<TestEntity> = GenericFeeder::new("test_id".to_string());
276        feeder.feed(None);
277        assert!(feeder.data.is_none());
278    }
279}