fitbit_rs/
response_cache.rs

1//! Cache for Fitbit API responses.
2//!
3//! This module provides a caching mechanism for Fitbit API responses to reduce the number
4//! of API calls made.
5
6use crate::activity_summary::ActivitySummaryResponse;
7use crate::error::FitbitError;
8use crate::fitbit_client::FitbitClientTrait;
9use crate::sleep::SleepResponseV1_2;
10use chrono::NaiveDate;
11use std::collections::HashMap;
12
13/// A cache for Fitbit API responses.
14///
15/// This cache stores responses from the Fitbit API to reduce the number of API calls
16/// made. It caches responses by date, so multiple requests for the same date will
17/// only result in a single API call.
18pub struct FitbitResponseCache<C: FitbitClientTrait> {
19    fitbit_client: C,
20    sleep_responses: HashMap<NaiveDate, SleepResponseV1_2>,
21    activity_summary_responses: HashMap<NaiveDate, ActivitySummaryResponse>,
22}
23
24impl<C: FitbitClientTrait> FitbitResponseCache<C> {
25    /// Creates a new cache with the given Fitbit client.
26    ///
27    /// # Arguments
28    ///
29    /// * `fitbit_client` - The Fitbit client to use for making API calls
30    ///
31    /// # Example
32    ///
33    /// ```
34    /// use fitbit_rs::{FitbitClient, FitbitResponseCache};
35    ///
36    /// let client = FitbitClient::new("your_access_token".to_string());
37    /// let cache = FitbitResponseCache::new(client);
38    /// ```
39    pub fn new(fitbit_client: C) -> Self {
40        Self {
41            fitbit_client,
42            sleep_responses: HashMap::new(),
43            activity_summary_responses: HashMap::new(),
44        }
45    }
46
47    /// Gets a sleep response for the given date.
48    ///
49    /// If the response is not in the cache, it will be fetched from the API and cached.
50    ///
51    /// # Arguments
52    ///
53    /// * `date` - The date for which to get sleep data
54    ///
55    /// # Returns
56    ///
57    /// A reference to the cached sleep response or an error if the request failed
58    ///
59    /// # Example
60    ///
61    /// ```
62    /// # use fitbit_rs::{FitbitClient, FitbitResponseCache};
63    /// # use chrono::NaiveDate;
64    /// #
65    /// # let client = FitbitClient::new("your_access_token".to_string());
66    /// # let mut cache = FitbitResponseCache::new(client);
67    /// #
68    /// let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
69    /// let sleep_data = cache.get_sleep_response(date);
70    /// ```
71    pub fn get_sleep_response(
72        &mut self,
73        date: NaiveDate,
74    ) -> Result<&SleepResponseV1_2, FitbitError> {
75        if !self.sleep_responses.contains_key(&date) {
76            let response = self.fitbit_client.fetch_sleep_data(date)?;
77            self.sleep_responses.insert(date, response);
78        }
79
80        Ok(self.sleep_responses.get(&date).unwrap())
81    }
82
83    /// Gets an activity summary response for the given date.
84    ///
85    /// If the response is not in the cache, it will be fetched from the API and cached.
86    ///
87    /// # Arguments
88    ///
89    /// * `date` - The date for which to get activity data
90    ///
91    /// # Returns
92    ///
93    /// A reference to the cached activity summary response or an error if the request failed
94    ///
95    /// # Example
96    ///
97    /// ```
98    /// # use fitbit_rs::{FitbitClient, FitbitResponseCache};
99    /// # use chrono::NaiveDate;
100    /// #
101    /// # let client = FitbitClient::new("your_access_token".to_string());
102    /// # let mut cache = FitbitResponseCache::new(client);
103    /// #
104    /// let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
105    /// let activity_data = cache.get_activity_summary_response(date);
106    /// ```
107    pub fn get_activity_summary_response(
108        &mut self,
109        date: NaiveDate,
110    ) -> Result<&ActivitySummaryResponse, FitbitError> {
111        if !self.activity_summary_responses.contains_key(&date) {
112            let response = self.fitbit_client.fetch_activity_summary(date)?;
113            self.activity_summary_responses.insert(date, response);
114        }
115
116        Ok(self.activity_summary_responses.get(&date).unwrap())
117    }
118
119    /// Clears all cached responses.
120    ///
121    /// This can be useful if you want to force a refresh of all data.
122    pub fn clear_cache(&mut self) {
123        self.sleep_responses.clear();
124        self.activity_summary_responses.clear();
125    }
126
127    /// Removes a specific date from the cache.
128    ///
129    /// This can be useful if you want to force a refresh of data for a specific date.
130    ///
131    /// # Arguments
132    ///
133    /// * `date` - The date to remove from the cache
134    pub fn remove_from_cache(&mut self, date: NaiveDate) {
135        self.sleep_responses.remove(&date);
136        self.activity_summary_responses.remove(&date);
137    }
138
139    /// Gets a reference to the underlying Fitbit client.
140    ///
141    /// # Returns
142    ///
143    /// A reference to the Fitbit client
144    pub fn client(&self) -> &C {
145        &self.fitbit_client
146    }
147}
148
149#[cfg(test)]
150mod response_cache_tests {
151    use super::*;
152    use crate::fitbit_client::MockFitbitClientTrait;
153    use chrono::NaiveDate;
154    use mockall::predicate::*;
155
156    #[test]
157    fn test_cache_behavior() -> Result<(), FitbitError> {
158        let mut mock_client = MockFitbitClientTrait::new();
159        let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
160
161        // Setup mock expectations - sleep data should only be called once
162        mock_client
163            .expect_fetch_sleep_data()
164            .with(eq(date))
165            .times(1)
166            .returning(|_| Ok(create_mock_sleep_response()));
167
168        let mut cache = FitbitResponseCache::new(mock_client);
169
170        // First call should fetch from API
171        let _response1 = cache.get_sleep_response(date)?;
172        // Second call should use cached data
173        let _response2 = cache.get_sleep_response(date)?;
174
175        Ok(())
176    }
177
178    #[test]
179    fn test_clear_cache() -> Result<(), FitbitError> {
180        let mut mock_client = MockFitbitClientTrait::new();
181        let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
182
183        // Setup mock expectations - should be called twice due to cache clearing
184        mock_client
185            .expect_fetch_sleep_data()
186            .with(eq(date))
187            .times(2)
188            .returning(|_| Ok(create_mock_sleep_response()));
189
190        let mut cache = FitbitResponseCache::new(mock_client);
191
192        // First call should fetch from API
193        let _response1 = cache.get_sleep_response(date)?;
194
195        // Clear cache
196        cache.clear_cache();
197
198        // Next call should fetch from API again
199        let _response2 = cache.get_sleep_response(date)?;
200
201        Ok(())
202    }
203
204    #[test]
205    fn test_remove_from_cache() -> Result<(), FitbitError> {
206        let mut mock_client = MockFitbitClientTrait::new();
207        let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
208
209        // Setup mock expectations - should be called twice due to cache removal
210        mock_client
211            .expect_fetch_sleep_data()
212            .with(eq(date))
213            .times(2)
214            .returning(|_| Ok(create_mock_sleep_response()));
215
216        let mut cache = FitbitResponseCache::new(mock_client);
217
218        // First call should fetch from API
219        let _response1 = cache.get_sleep_response(date)?;
220
221        // Remove specific date from cache
222        cache.remove_from_cache(date);
223
224        // Next call for same date should fetch from API again
225        let _response2 = cache.get_sleep_response(date)?;
226
227        Ok(())
228    }
229
230    fn create_mock_sleep_response() -> SleepResponseV1_2 {
231        SleepResponseV1_2::default()
232    }
233}