use crate::cli::DagType;
use crate::models::churn::CodeChurnAnalysis;
use crate::models::dag::DependencyGraph;
use crate::models::template::TemplateResource;
use crate::services::cache::base::CacheStrategy;
use crate::services::context::FileContext;
use std::fs;
use std::path::PathBuf;
use std::time::{Duration, UNIX_EPOCH};
#[derive(Clone)]
pub struct AstCacheStrategy;
impl CacheStrategy for AstCacheStrategy {
type Key = PathBuf;
type Value = FileContext;
fn cache_key(&self, path: &PathBuf) -> String {
let mtime = fs::metadata(path)
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map_or(0, |d| d.as_secs());
format!("ast:{}:{}", path.display(), mtime)
}
fn validate(&self, path: &PathBuf, cached: &FileContext) -> bool {
if !path.exists() {
return false;
}
let current_mtime = fs::metadata(path)
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map_or(0, |d| d.as_secs());
let cached_path = PathBuf::from(&cached.path);
if cached_path != *path {
return false;
}
if let Ok(cached_metadata) = fs::metadata(&cached.path) {
if let Ok(cached_modified) = cached_metadata.modified() {
if let Ok(cached_duration) = cached_modified.duration_since(UNIX_EPOCH) {
let file_mtime = cached_duration.as_secs();
return current_mtime == file_mtime;
}
}
}
false
}
fn ttl(&self) -> Option<Duration> {
Some(Duration::from_secs(300)) }
fn max_size(&self) -> usize {
100 }
}
#[derive(Clone)]
pub struct TemplateCacheStrategy;
impl CacheStrategy for TemplateCacheStrategy {
type Key = String; type Value = TemplateResource;
fn cache_key(&self, uri: &String) -> String {
format!("template:{uri}")
}
fn validate(&self, _uri: &String, _cached: &TemplateResource) -> bool {
true
}
fn ttl(&self) -> Option<Duration> {
Some(Duration::from_secs(600)) }
fn max_size(&self) -> usize {
50 }
}
#[derive(Clone)]
pub struct DagCacheStrategy;
impl CacheStrategy for DagCacheStrategy {
type Key = (PathBuf, DagType);
type Value = DependencyGraph;
fn cache_key(&self, (path, dag_type): &(PathBuf, DagType)) -> String {
format!("dag:{}:{:?}", path.display(), dag_type)
}
fn validate(&self, (path, _): &(PathBuf, DagType), cached: &DependencyGraph) -> bool {
if !path.exists() {
return false;
}
for node in cached.nodes.values().take(10) {
let file_path = PathBuf::from(&node.file_path);
if file_path.exists() {
if let Ok(metadata) = fs::metadata(&file_path) {
if let Ok(modified) = metadata.modified() {
if let Ok(elapsed) = modified.elapsed() {
if elapsed.as_secs() < 2 {
return false;
}
}
}
}
}
}
true
}
fn ttl(&self) -> Option<Duration> {
Some(Duration::from_secs(180)) }
fn max_size(&self) -> usize {
20 }
}
#[derive(Clone)]
pub struct ChurnCacheStrategy;
impl CacheStrategy for ChurnCacheStrategy {
type Key = (PathBuf, u32); type Value = CodeChurnAnalysis;
fn cache_key(&self, (repo, period_days): &(PathBuf, u32)) -> String {
let head = self.get_git_head(repo).unwrap_or_default();
format!("churn:{}:{}:{}", repo.display(), period_days, head)
}
fn validate(&self, (repo, _): &(PathBuf, u32), _cached: &CodeChurnAnalysis) -> bool {
if let Some(_current_head) = self.get_git_head(repo) {
true
} else {
false
}
}
fn ttl(&self) -> Option<Duration> {
Some(Duration::from_secs(1800)) }
fn max_size(&self) -> usize {
20 }
}
impl ChurnCacheStrategy {
fn get_git_head(&self, repo: &PathBuf) -> Option<String> {
std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(repo)
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
}
}
#[derive(Clone)]
pub struct GitStatsCacheStrategy;
#[derive(Clone)]
pub struct GitStats {
pub total_commits: usize,
pub authors: Vec<String>,
pub branch: String,
pub head_commit: String,
}
impl CacheStrategy for GitStatsCacheStrategy {
type Key = PathBuf;
type Value = GitStats;
fn cache_key(&self, repo: &PathBuf) -> String {
let branch = self
.get_current_branch(repo)
.unwrap_or_else(|| "unknown".to_string());
format!("git_stats:{}:{}", repo.display(), branch)
}
fn validate(&self, repo: &PathBuf, cached: &GitStats) -> bool {
self.get_git_head(repo)
.is_some_and(|head| head == cached.head_commit)
}
fn ttl(&self) -> Option<Duration> {
Some(Duration::from_secs(900)) }
fn max_size(&self) -> usize {
10 }
}
impl GitStatsCacheStrategy {
fn get_current_branch(&self, repo: &PathBuf) -> Option<String> {
std::process::Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.current_dir(repo)
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
}
fn get_git_head(&self, repo: &PathBuf) -> Option<String> {
std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(repo)
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use tempfile::TempDir;
#[test]
fn test_ast_cache_strategy() {
let strategy = AstCacheStrategy;
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
File::create(&test_file).unwrap();
let key = strategy.cache_key(&test_file);
assert!(key.starts_with("ast:"));
assert!(key.contains("test.rs"));
assert_eq!(strategy.ttl(), Some(Duration::from_secs(300)));
assert_eq!(strategy.max_size(), 100);
}
#[test]
fn test_template_cache_strategy() {
let strategy = TemplateCacheStrategy;
let key = strategy.cache_key(&"template:test".to_string());
assert_eq!(key, "template:template:test");
assert_eq!(strategy.ttl(), Some(Duration::from_secs(600)));
assert_eq!(strategy.max_size(), 50);
}
#[test]
fn test_dag_cache_strategy() {
let strategy = DagCacheStrategy;
let key = strategy.cache_key(&(PathBuf::from("/test"), DagType::ImportGraph));
assert!(key.contains("dag:"));
assert!(key.contains("ImportGraph"));
assert_eq!(strategy.ttl(), Some(Duration::from_secs(180)));
assert_eq!(strategy.max_size(), 20);
}
#[test]
fn test_churn_cache_strategy() {
let strategy = ChurnCacheStrategy;
let temp_dir = TempDir::new().unwrap();
let key = strategy.cache_key(&(temp_dir.path().to_path_buf(), 30));
assert!(key.starts_with("churn:"));
assert_eq!(strategy.ttl(), Some(Duration::from_secs(1800)));
assert_eq!(strategy.max_size(), 20);
}
#[test]
fn test_git_stats_cache_strategy() {
let strategy = GitStatsCacheStrategy;
let temp_dir = TempDir::new().unwrap();
let key = strategy.cache_key(&temp_dir.path().to_path_buf());
assert!(key.starts_with("git_stats:"));
assert_eq!(strategy.ttl(), Some(Duration::from_secs(900)));
assert_eq!(strategy.max_size(), 10);
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}