use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub default_ttl: Duration,
pub max_items: usize,
pub compress: bool,
pub enable_metrics: bool,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
default_ttl: Duration::from_secs(300), max_items: 1000,
compress: true,
enable_metrics: true,
}
}
}
#[derive(Debug, Clone)]
struct CacheEntry<T> {
data: T,
created_at: Instant,
ttl: Duration,
access_count: u64,
last_access: Instant,
}
impl<T> CacheEntry<T> {
fn new(data: T, ttl: Duration) -> Self {
let now = Instant::now();
Self {
data,
created_at: now,
ttl,
access_count: 0,
last_access: now,
}
}
fn is_expired(&self) -> bool {
self.created_at.elapsed() > self.ttl
}
fn access(&mut self) -> &T {
self.access_count += 1;
self.last_access = Instant::now();
&self.data
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
endpoint: String,
params: String,
}
impl CacheKey {
pub fn new(endpoint: &str, params: &str) -> Self {
Self {
endpoint: endpoint.to_string(),
params: params.to_string(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheMetrics {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub expired_items: u64,
pub total_items: usize,
}
impl CacheMetrics {
pub fn hit_rate(&self) -> f64 {
if self.hits + self.misses == 0 {
0.0
} else {
self.hits as f64 / (self.hits + self.misses) as f64
}
}
}
pub struct IntelligentCache<T>
where
T: Clone + Send + Sync,
{
cache: Arc<RwLock<HashMap<CacheKey, CacheEntry<T>>>>,
config: CacheConfig,
metrics: Arc<RwLock<CacheMetrics>>,
}
impl<T> IntelligentCache<T>
where
T: Clone + Send + Sync,
{
pub fn new(config: CacheConfig) -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
config,
metrics: Arc::new(RwLock::new(CacheMetrics::default())),
}
}
pub async fn get(&self, key: &CacheKey) -> Option<T> {
let mut cache = self.cache.write().await;
if let Some(entry) = cache.get_mut(key) {
if entry.is_expired() {
cache.remove(key);
if self.config.enable_metrics {
let mut metrics = self.metrics.write().await;
metrics.expired_items += 1;
metrics.total_items = cache.len();
}
return None;
}
let data = entry.access().clone();
if self.config.enable_metrics {
let mut metrics = self.metrics.write().await;
metrics.hits += 1;
}
Some(data)
} else {
if self.config.enable_metrics {
let mut metrics = self.metrics.write().await;
metrics.misses += 1;
}
None
}
}
pub async fn set(&self, key: CacheKey, value: T, ttl: Option<Duration>) {
let mut cache = self.cache.write().await;
if cache.len() >= self.config.max_items {
self.evict_lru(&mut cache).await;
}
let ttl = ttl.unwrap_or(self.config.default_ttl);
let entry = CacheEntry::new(value, ttl);
cache.insert(key, entry);
if self.config.enable_metrics {
let mut metrics = self.metrics.write().await;
metrics.total_items = cache.len();
}
}
async fn evict_lru(&self, cache: &mut HashMap<CacheKey, CacheEntry<T>>) {
if cache.is_empty() {
return;
}
let lru_key = cache
.iter()
.min_by_key(|(_, entry)| entry.last_access)
.map(|(key, _)| key.clone());
if let Some(key) = lru_key {
cache.remove(&key);
if self.config.enable_metrics {
let mut metrics = self.metrics.write().await;
metrics.evictions += 1;
}
}
}
pub async fn cleanup_expired(&self) {
let mut cache = self.cache.write().await;
let initial_size = cache.len();
cache.retain(|_, entry| !entry.is_expired());
if self.config.enable_metrics {
let mut metrics = self.metrics.write().await;
metrics.expired_items += (initial_size - cache.len()) as u64;
metrics.total_items = cache.len();
}
}
pub async fn get_metrics(&self) -> CacheMetrics {
self.metrics.read().await.clone()
}
pub async fn clear(&self) {
let mut cache = self.cache.write().await;
cache.clear();
if self.config.enable_metrics {
let mut metrics = self.metrics.write().await;
metrics.total_items = 0;
}
}
pub async fn size(&self) -> usize {
self.cache.read().await.len()
}
}
pub struct SmartCacheStrategy;
impl SmartCacheStrategy {
pub fn get_ttl_for_endpoint(endpoint: &str) -> Duration {
match endpoint {
path if path.contains("quote") || path.contains("price") => Duration::from_secs(30),
path if path.contains("market-hours") => Duration::from_secs(3600 * 12),
path if path.contains("profile") || path.contains("company") => {
Duration::from_secs(3600 * 24)
}
path if path.contains("income-statement") || path.contains("balance-sheet") => {
Duration::from_secs(3600 * 6)
}
path if path.contains("historical") => Duration::from_secs(3600 * 2),
path if path.contains("news") || path.contains("calendar") => Duration::from_secs(900),
path if path.contains("symbols") || path.contains("exchanges") => {
Duration::from_secs(3600 * 24 * 7)
}
_ => Duration::from_secs(300),
}
}
pub fn should_cache_endpoint(endpoint: &str) -> bool {
if endpoint.contains("bulk") {
return false;
}
if endpoint.contains("stream") || endpoint.contains("websocket") {
return false;
}
true
}
pub fn generate_cache_key(endpoint: &str, query_params: Option<&str>) -> CacheKey {
let params = query_params.unwrap_or("");
CacheKey::new(endpoint, params)
}
}
pub struct CachedApiClient<T>
where
T: Clone + Send + Sync + for<'de> Deserialize<'de> + Serialize,
{
cache: IntelligentCache<T>,
}
impl<T> CachedApiClient<T>
where
T: Clone + Send + Sync + for<'de> Deserialize<'de> + Serialize,
{
pub fn new(config: CacheConfig) -> Self {
Self {
cache: IntelligentCache::new(config),
}
}
pub async fn get_cached<F, Fut>(&self, key: CacheKey, fetch_fn: F) -> Result<T>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
if let Some(cached_data) = self.cache.get(&key).await {
return Ok(cached_data);
}
let data = fetch_fn().await?;
if SmartCacheStrategy::should_cache_endpoint(&key.endpoint) {
let ttl = SmartCacheStrategy::get_ttl_for_endpoint(&key.endpoint);
self.cache.set(key, data.clone(), Some(ttl)).await;
}
Ok(data)
}
pub async fn metrics(&self) -> CacheMetrics {
self.cache.get_metrics().await
}
pub async fn cleanup(&self) {
self.cache.cleanup_expired().await;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_cache_basic_operations() {
let config = CacheConfig::default();
let cache: IntelligentCache<String> = IntelligentCache::new(config);
let key = CacheKey::new("test", "params");
let value = "test_value".to_string();
assert!(cache.get(&key).await.is_none());
cache.set(key.clone(), value.clone(), None).await;
assert_eq!(cache.get(&key).await.unwrap(), value);
}
#[tokio::test]
async fn test_cache_expiration() {
let config = CacheConfig::default();
let cache: IntelligentCache<String> = IntelligentCache::new(config);
let key = CacheKey::new("test", "params");
let value = "test_value".to_string();
cache
.set(key.clone(), value, Some(Duration::from_millis(100)))
.await;
assert!(cache.get(&key).await.is_some());
tokio::time::sleep(Duration::from_millis(150)).await;
assert!(cache.get(&key).await.is_none());
}
#[test]
fn test_smart_cache_strategy() {
assert_eq!(
SmartCacheStrategy::get_ttl_for_endpoint("/quote"),
Duration::from_secs(30)
);
assert_eq!(
SmartCacheStrategy::get_ttl_for_endpoint("/profile"),
Duration::from_secs(3600 * 24)
);
assert!(SmartCacheStrategy::should_cache_endpoint("/quote"));
assert!(!SmartCacheStrategy::should_cache_endpoint("/bulk-data"));
}
}