use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::{
collections::HashMap,
fs,
io::Write,
path::PathBuf,
time::{Duration, SystemTime},
};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq, Default)]
pub enum FlushPolicy {
Auto,
#[default]
Manual,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Entry {
timestamp: SystemTime,
data: serde_json::Value,
}
#[derive(Debug)]
pub struct Cache {
path: PathBuf,
entries: HashMap<String, Entry>,
entry_ttl: Duration,
flush_policy: FlushPolicy,
}
impl Cache {
pub fn new(path: PathBuf, entry_ttl: Duration, flush_policy: FlushPolicy) -> Result<Self> {
let entries = match fs::read_to_string(&path) {
Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(),
Err(_) => {
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
fs::create_dir_all(parent)?;
}
HashMap::new()
}
};
Ok(Cache {
path,
entries,
entry_ttl,
flush_policy,
})
}
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
self.entries.get(key).and_then(|entry| {
let diff = SystemTime::now().duration_since(entry.timestamp).unwrap();
if diff.lt(&self.entry_ttl) {
serde_json::from_value(entry.data.clone()).ok()
} else {
None
}
})
}
pub fn get_or<T, F, E>(&mut self, key: &str, fetch: F) -> std::result::Result<T, E>
where
T: Serialize + DeserializeOwned,
F: FnOnce() -> std::result::Result<T, E>,
E: From<Error>,
{
if let Some(data) = self.get::<T>(key) {
return Ok(data);
}
let value = fetch()?;
self.set(key, value).map_err(E::from)
}
pub fn set<T: Serialize>(&mut self, key: &str, value: T) -> Result<T> {
self.entries.insert(
key.to_string(),
Entry {
timestamp: SystemTime::now(),
data: serde_json::to_value(&value)?,
},
);
if self.flush_policy == FlushPolicy::Auto {
self.flush()?;
}
Ok(value)
}
pub fn flush(&self) -> Result<()> {
let mut file = fs::File::create(&self.path)?;
file.write_all(serde_json::to_string(&self.entries)?.as_bytes())?;
Ok(())
}
pub fn clear(&mut self) -> Result<()> {
self.entries.clear();
if self.flush_policy == FlushPolicy::Auto {
self.flush()?;
};
Ok(())
}
}