pub mod file_cache;
pub use file_cache::FileCache;
use anyhow::Result;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey(pub(crate) String);
impl CacheKey {
pub fn from_parts(parts: &[&str]) -> Self {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
for p in parts {
h.update(p.as_bytes());
h.update(b"\0"); }
let full = format!("{:x}", h.finalize());
Self(full[..16].to_string()) }
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub folder: PathBuf,
pub max_size_bytes: u64,
}
impl CacheConfig {
pub fn new(folder: impl Into<PathBuf>, max_size_bytes: u64) -> Self {
Self {
folder: folder.into(),
max_size_bytes,
}
}
pub fn clear(&self) -> Result<()> {
let entries = match std::fs::read_dir(&self.folder) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e.into()),
};
let mut errors: Vec<String> = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "cache")
&& let Err(e) = std::fs::remove_file(&path)
{
errors.push(format!("{}: {}", path.display(), e));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(anyhow::anyhow!(
"cache clear errors:\n{}",
errors.join("\n")
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_uniqueness() {
let k1 = CacheKey::from_parts(&["localhost", "5000", "select from trades where date=0"]);
let k2 = CacheKey::from_parts(&["localhost", "5000", "select from trades where date=1"]);
assert_ne!(k1, k2);
}
#[test]
fn test_key_same_input_same_output() {
let k1 = CacheKey::from_parts(&["localhost", "5000", "select from trades"]);
let k2 = CacheKey::from_parts(&["localhost", "5000", "select from trades"]);
assert_eq!(k1, k2);
}
#[test]
fn test_key_separator_prevents_collision() {
let k1 = CacheKey::from_parts(&["ab", "c", "q"]);
let k2 = CacheKey::from_parts(&["a", "bc", "q"]);
assert_ne!(k1, k2);
}
#[test]
fn test_key_stability() {
let key = CacheKey::from_parts(&["localhost", "5000", "select from trades"]);
assert_eq!(key.0, "5899c93491e25e68");
}
#[test]
fn test_cache_config_clear() {
let dir = std::env::temp_dir().join(format!(
"wingfoil_cache_config_clear_{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("a.cache"), b"data").unwrap();
std::fs::write(dir.join("b.cache"), b"data").unwrap();
std::fs::write(dir.join("other.txt"), b"keep").unwrap();
let config = CacheConfig::new(&dir, u64::MAX);
config.clear().unwrap();
assert!(!dir.join("a.cache").exists());
assert!(!dir.join("b.cache").exists());
assert!(dir.join("other.txt").exists());
let absent = CacheConfig::new(dir.join("nonexistent"), u64::MAX);
absent.clear().unwrap();
std::fs::remove_dir_all(&dir).unwrap();
}
}