barad-dur 0.17.3

The all-seeing repository analyzer
Documentation
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;

const CACHE_FILE: &str = ".repository-analysis/deps-cache.json";
const TTL_DAYS: i64 = 7;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheEntry {
    pub current_published: Option<DateTime<Utc>>,
    pub latest_version: Option<String>,
    pub latest_published: Option<DateTime<Utc>>,
    pub vulnerabilities: Vec<crate::deps::Vuln>,
    pub cached_at: DateTime<Utc>,
}

impl CacheEntry {
    pub fn is_fresh(&self) -> bool {
        Utc::now() - self.cached_at < Duration::days(TTL_DAYS)
    }
}

pub type DepsCache = HashMap<String, CacheEntry>;

pub fn cache_key(ecosystem: &str, name: &str, version: &str) -> String {
    format!(
        "{}:{}:{}",
        ecosystem.to_lowercase(),
        name.to_lowercase(),
        version
    )
}

pub fn load(repo_root: &Path) -> DepsCache {
    let path = repo_root.join(CACHE_FILE);
    std::fs::read_to_string(&path)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default()
}

pub fn save(repo_root: &Path, cache: &DepsCache) {
    let path = repo_root.join(CACHE_FILE);
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    if let Ok(json) = serde_json::to_string_pretty(cache) {
        let _ = std::fs::write(&path, json);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    #[test]
    fn cache_key_is_lowercase() {
        assert_eq!(cache_key("Cargo", "Serde", "1.0.0"), "cargo:serde:1.0.0");
        assert_eq!(
            cache_key("NuGet", "Newtonsoft.Json", "13.0.3"),
            "nuget:newtonsoft.json:13.0.3"
        );
    }

    #[test]
    fn fresh_entry_within_ttl() {
        let entry = CacheEntry {
            current_published: None,
            latest_version: None,
            latest_published: None,
            vulnerabilities: vec![],
            cached_at: Utc::now(),
        };
        assert!(entry.is_fresh());
    }

    #[test]
    fn stale_entry_beyond_ttl() {
        let entry = CacheEntry {
            current_published: None,
            latest_version: None,
            latest_published: None,
            vulnerabilities: vec![],
            cached_at: Utc::now() - Duration::days(8),
        };
        assert!(!entry.is_fresh());
    }

    #[test]
    fn roundtrip_save_load() {
        let dir = tempdir().unwrap();
        let mut cache: DepsCache = HashMap::new();
        cache.insert(
            "cargo:serde:1.0.130".into(),
            CacheEntry {
                current_published: Some(Utc::now()),
                latest_version: Some("1.0.197".into()),
                latest_published: Some(Utc::now()),
                vulnerabilities: vec![],
                cached_at: Utc::now(),
            },
        );
        save(dir.path(), &cache);
        let loaded = load(dir.path());
        assert!(loaded.contains_key("cargo:serde:1.0.130"));
    }
}