use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
use super::entry::CacheEntry;
use super::key::{CacheCategory, CacheKey};
use super::stats::CacheStats;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum CacheBackend {
Filesystem {
path: PathBuf,
},
Memory,
#[cfg(feature = "redis")]
Redis {
url: String,
#[serde(default = "default_redis_ttl")]
ttl_secs: u64,
},
}
#[cfg(feature = "redis")]
fn default_redis_ttl() -> u64 {
3600 }
impl Default for CacheBackend {
fn default() -> Self {
CacheBackend::Filesystem {
path: default_cache_path(),
}
}
}
pub fn default_cache_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".mcplint")
.join("cache")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
pub backend: CacheBackend,
#[serde(default = "default_schema_ttl")]
pub schema_ttl_secs: u64,
#[serde(default = "default_result_ttl")]
pub result_ttl_secs: u64,
#[serde(default = "default_validation_ttl")]
pub validation_ttl_secs: u64,
#[serde(default = "default_corpus_persist")]
pub corpus_persist: bool,
pub max_size_bytes: Option<u64>,
#[serde(default = "default_enabled")]
pub enabled: bool,
}
fn default_schema_ttl() -> u64 {
3600 }
fn default_result_ttl() -> u64 {
86400 }
fn default_validation_ttl() -> u64 {
3600 }
fn default_corpus_persist() -> bool {
true
}
fn default_enabled() -> bool {
true
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
backend: CacheBackend::default(),
schema_ttl_secs: default_schema_ttl(),
result_ttl_secs: default_result_ttl(),
validation_ttl_secs: default_validation_ttl(),
corpus_persist: default_corpus_persist(),
max_size_bytes: None,
enabled: default_enabled(),
}
}
}
impl CacheConfig {
pub fn memory() -> Self {
Self {
backend: CacheBackend::Memory,
..Default::default()
}
}
pub fn filesystem(path: PathBuf) -> Self {
Self {
backend: CacheBackend::Filesystem { path },
..Default::default()
}
}
#[cfg(feature = "redis")]
pub fn redis(url: &str) -> Self {
Self {
backend: CacheBackend::Redis {
url: url.to_string(),
ttl_secs: default_redis_ttl(),
},
..Default::default()
}
}
pub fn ttl_for_category(&self, category: CacheCategory) -> Duration {
match category {
CacheCategory::Schema => Duration::from_secs(self.schema_ttl_secs),
CacheCategory::ScanResult => Duration::from_secs(self.result_ttl_secs),
CacheCategory::Validation => Duration::from_secs(self.validation_ttl_secs),
CacheCategory::Corpus => Duration::from_secs(86400 * 365), CacheCategory::ToolHash => Duration::from_secs(86400 * 365), CacheCategory::AiResponse => Duration::from_secs(3600), }
}
pub fn with_max_size(mut self, bytes: u64) -> Self {
self.max_size_bytes = Some(bytes);
self
}
pub fn disabled() -> Self {
Self {
enabled: false,
..Default::default()
}
}
}
#[async_trait]
pub trait Cache: Send + Sync {
async fn get(&self, key: &CacheKey) -> Result<Option<CacheEntry>>;
async fn set(&self, key: &CacheKey, entry: CacheEntry) -> Result<()>;
async fn delete(&self, key: &CacheKey) -> Result<()>;
async fn exists(&self, key: &CacheKey) -> Result<bool>;
async fn clear(&self, category: Option<CacheCategory>) -> Result<u64>;
async fn stats(&self) -> Result<CacheStats>;
async fn prune_expired(&self) -> Result<u64>;
async fn keys(&self, category: Option<CacheCategory>) -> Result<Vec<CacheKey>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let config = CacheConfig::default();
assert!(config.enabled);
assert!(config.corpus_persist);
assert_eq!(config.schema_ttl_secs, 3600);
}
#[test]
fn memory_config() {
let config = CacheConfig::memory();
assert!(matches!(config.backend, CacheBackend::Memory));
}
#[test]
fn ttl_for_category() {
let config = CacheConfig::default();
assert_eq!(
config.ttl_for_category(CacheCategory::Schema).as_secs(),
3600
);
assert_eq!(
config.ttl_for_category(CacheCategory::ScanResult).as_secs(),
86400
);
}
#[test]
fn disabled_config() {
let config = CacheConfig::disabled();
assert!(!config.enabled);
}
}