use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
use crate::models::{Item, Riven};
#[derive(Debug, Default)]
pub struct ApiCache {
items: Option<CacheEntry<Vec<Item>>>,
rivens: Option<CacheEntry<Vec<Riven>>>,
}
#[derive(Debug)]
struct CacheEntry<T> {
data: T,
fetched_at: Instant,
}
impl ApiCache {
pub fn new() -> Self {
Self::default()
}
pub fn has_items(&self) -> bool {
self.items.is_some()
}
pub fn has_rivens(&self) -> bool {
self.rivens.is_some()
}
pub fn items_age(&self) -> Option<Duration> {
self.items.as_ref().map(|e| e.fetched_at.elapsed())
}
pub fn rivens_age(&self) -> Option<Duration> {
self.rivens.as_ref().map(|e| e.fetched_at.elapsed())
}
pub fn clear(&mut self) {
self.items = None;
self.rivens = None;
}
pub fn invalidate_items(&mut self) {
self.items = None;
}
pub fn invalidate_rivens(&mut self) {
self.rivens = None;
}
pub fn invalidate_items_if_older_than(&mut self, max_age: Duration) -> bool {
if self.items_age().is_some_and(|age| age > max_age) {
self.invalidate_items();
true
} else {
false
}
}
pub fn invalidate_rivens_if_older_than(&mut self, max_age: Duration) -> bool {
if self.rivens_age().is_some_and(|age| age > max_age) {
self.invalidate_rivens();
true
} else {
false
}
}
pub(crate) fn get_items(&self) -> Option<&[Item]> {
self.items.as_ref().map(|e| e.data.as_slice())
}
pub(crate) fn set_items(&mut self, items: Vec<Item>) {
self.items = Some(CacheEntry {
data: items,
fetched_at: Instant::now(),
});
}
pub(crate) fn get_rivens(&self) -> Option<&[Riven]> {
self.rivens.as_ref().map(|e| e.data.as_slice())
}
pub(crate) fn set_rivens(&mut self, rivens: Vec<Riven>) {
self.rivens = Some(CacheEntry {
data: rivens,
fetched_at: Instant::now(),
});
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableCache {
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<CachedItems>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rivens: Option<CachedRivens>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedItems {
pub data: Vec<Item>,
pub fetched_at_unix: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedRivens {
pub data: Vec<Riven>,
pub fetched_at_unix: u64,
}
impl SerializableCache {
pub fn new() -> Self {
Self {
items: None,
rivens: None,
}
}
pub fn into_api_cache(self) -> ApiCache {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mut cache = ApiCache::new();
if let Some(items) = self.items {
let age_secs = now.saturating_sub(items.fetched_at_unix);
cache.items = Some(CacheEntry {
data: items.data,
fetched_at: Instant::now() - Duration::from_secs(age_secs),
});
}
if let Some(rivens) = self.rivens {
let age_secs = now.saturating_sub(rivens.fetched_at_unix);
cache.rivens = Some(CacheEntry {
data: rivens.data,
fetched_at: Instant::now() - Duration::from_secs(age_secs),
});
}
cache
}
}
impl Default for SerializableCache {
fn default() -> Self {
Self::new()
}
}
impl From<&ApiCache> for SerializableCache {
fn from(cache: &ApiCache) -> Self {
let now_unix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
items: cache.items.as_ref().map(|e| {
let age = e.fetched_at.elapsed().as_secs();
CachedItems {
data: e.data.clone(),
fetched_at_unix: now_unix.saturating_sub(age),
}
}),
rivens: cache.rivens.as_ref().map(|e| {
let age = e.fetched_at.elapsed().as_secs();
CachedRivens {
data: e.data.clone(),
fetched_at_unix: now_unix.saturating_sub(age),
}
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_new_is_empty() {
let cache = ApiCache::new();
assert!(!cache.has_items());
assert!(!cache.has_rivens());
}
#[test]
fn test_cache_set_and_get_items() {
let mut cache = ApiCache::new();
cache.set_items(vec![]);
assert!(cache.has_items());
assert!(cache.get_items().is_some());
}
#[test]
fn test_cache_invalidation() {
let mut cache = ApiCache::new();
cache.set_items(vec![]);
cache.set_rivens(vec![]);
cache.invalidate_items();
assert!(!cache.has_items());
assert!(cache.has_rivens());
cache.clear();
assert!(!cache.has_rivens());
}
#[test]
fn test_cache_age() {
let mut cache = ApiCache::new();
assert!(cache.items_age().is_none());
cache.set_items(vec![]);
let age = cache.items_age().unwrap();
assert!(age < Duration::from_secs(1));
}
#[test]
fn test_serializable_cache_roundtrip() {
let mut cache = ApiCache::new();
cache.set_items(vec![]);
let serializable = SerializableCache::from(&cache);
assert!(serializable.items.is_some());
let json = serde_json::to_string(&serializable).unwrap();
let restored: SerializableCache = serde_json::from_str(&json).unwrap();
assert!(restored.items.is_some());
}
}