1use crate::api::ExchangeRateResponse;
4use chrono::{DateTime, Utc};
5use moka::future::Cache;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8use std::time::Duration;
9
10#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
12pub struct CacheKey {
13 pub base_currency: String,
15 pub date: Option<String>,
17}
18
19impl CacheKey {
20 pub fn latest(base_currency: &str) -> Self {
22 Self {
23 base_currency: base_currency.to_uppercase(),
24 date: None,
25 }
26 }
27
28 pub fn historical(base_currency: &str, date: &str) -> Self {
30 Self {
31 base_currency: base_currency.to_uppercase(),
32 date: Some(date.to_string()),
33 }
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct CachedRates {
40 pub data: ExchangeRateResponse,
42 pub cached_at: DateTime<Utc>,
44 pub expires_at: DateTime<Utc>,
46 pub access_count: u64,
48}
49
50impl CachedRates {
51 pub fn new(data: ExchangeRateResponse, ttl_seconds: u64) -> Self {
53 let now = Utc::now();
54 Self {
55 data,
56 cached_at: now,
57 expires_at: now + chrono::Duration::seconds(ttl_seconds as i64),
58 access_count: 0,
59 }
60 }
61
62 pub fn is_valid(&self) -> bool {
64 Utc::now() < self.expires_at
65 }
66
67 pub fn age_seconds(&self) -> i64 {
69 (Utc::now() - self.cached_at).num_seconds()
70 }
71
72 pub fn record_access(&mut self) {
74 self.access_count += 1;
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct CacheConfig {
81 pub max_capacity: u64,
83 pub latest_ttl: u64,
85 pub historical_ttl: u64,
87 pub enable_stats: bool,
89}
90
91impl Default for CacheConfig {
92 fn default() -> Self {
93 Self {
94 max_capacity: 1000,
95 latest_ttl: 300, historical_ttl: 3600, enable_stats: true,
98 }
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct ExchangeRateCache {
105 cache: Cache<CacheKey, Arc<CachedRates>>,
107 config: CacheConfig,
109 stats: Arc<dashmap::DashMap<String, u64>>,
111}
112
113impl ExchangeRateCache {
114 pub fn new() -> Self {
116 Self::with_config(CacheConfig::default())
117 }
118
119 pub fn with_config(config: CacheConfig) -> Self {
121 let cache = Cache::builder()
122 .max_capacity(config.max_capacity)
123 .time_to_live(Duration::from_secs(
124 config.latest_ttl.max(config.historical_ttl),
125 ))
126 .build();
127
128 Self {
129 cache,
130 config,
131 stats: Arc::new(dashmap::DashMap::new()),
132 }
133 }
134
135 pub async fn get(&self, key: &CacheKey) -> Option<ExchangeRateResponse> {
137 self.increment_stat("requests_total");
138
139 if let Some(cached) = self.cache.get(key).await {
140 if cached.is_valid() {
141 self.increment_stat("hits");
142
143 let mut updated = (*cached).clone();
145 updated.record_access();
146 self.cache.insert(key.clone(), Arc::new(updated)).await;
147
148 return Some(cached.data.clone());
149 } else {
150 self.cache.remove(key).await;
152 self.increment_stat("expired");
153 }
154 }
155
156 self.increment_stat("misses");
157 None
158 }
159
160 pub async fn put(&self, key: CacheKey, data: ExchangeRateResponse) {
162 let ttl = if key.date.is_some() {
163 self.config.historical_ttl
164 } else {
165 self.config.latest_ttl
166 };
167
168 let cached_rates = Arc::new(CachedRates::new(data, ttl));
169 self.cache.insert(key, cached_rates).await;
170 self.increment_stat("insertions");
171 }
172
173 pub fn get_stats(&self) -> CacheStats {
175 let total_requests = self.get_stat("requests_total");
176 let hits = self.get_stat("hits");
177 let misses = self.get_stat("misses");
178 let expired = self.get_stat("expired");
179
180 let hit_rate = if total_requests > 0 {
181 (hits as f64 / total_requests as f64) * 100.0
182 } else {
183 0.0
184 };
185
186 CacheStats {
187 total_requests,
188 hits,
189 misses,
190 expired,
191 hit_rate,
192 cached_entries: self.cache.entry_count(),
193 weighted_size: self.cache.weighted_size(),
194 }
195 }
196
197 pub async fn clear(&self) {
199 self.cache.invalidate_all();
200 if self.config.enable_stats {
201 self.stats.clear();
202 }
203 }
204
205 pub async fn cleanup_expired(&self) {
207 self.cache.run_pending_tasks().await;
209 }
210
211 pub async fn get_keys(&self) -> Vec<CacheKey> {
213 vec![]
216 }
217
218 fn increment_stat(&self, key: &str) {
220 if self.config.enable_stats {
221 self.stats
222 .entry(key.to_string())
223 .and_modify(|v| *v += 1)
224 .or_insert(1);
225 }
226 }
227
228 fn get_stat(&self, key: &str) -> u64 {
230 self.stats.get(key).map(|v| *v).unwrap_or(0)
231 }
232}
233
234impl Default for ExchangeRateCache {
235 fn default() -> Self {
236 Self::new()
237 }
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct CacheStats {
243 pub total_requests: u64,
245 pub hits: u64,
247 pub misses: u64,
249 pub expired: u64,
251 pub hit_rate: f64,
253 pub cached_entries: u64,
255 pub weighted_size: u64,
257}
258
259impl CacheStats {
260 pub fn format_summary(&self) -> String {
262 format!(
263 "Cache Stats: {:.1}% hit rate ({}/{} requests), {} entries, {} expired",
264 self.hit_rate, self.hits, self.total_requests, self.cached_entries, self.expired
265 )
266 }
267}