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}