es_fluent_cli/generation/
cache.rs

1//! Caching utilities for CLI performance optimization.
2//!
3//! This module provides caching for expensive operations like:
4//! - Cargo metadata results
5//! - Runner binary staleness detection via content hashing
6
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9
10/// Cache of cargo metadata results.
11///
12/// Stores extracted dependency info keyed by Cargo.lock hash to avoid
13/// running cargo_metadata on every invocation.
14#[derive(Debug, Default, Deserialize, Serialize)]
15pub struct MetadataCache {
16    /// Hash of Cargo.lock when cache was created
17    pub cargo_lock_hash: String,
18    /// Extracted es-fluent dependency string
19    pub es_fluent_dep: String,
20    /// Extracted es-fluent-cli-helpers dependency string
21    pub es_fluent_cli_helpers_dep: String,
22    /// Target directory
23    pub target_dir: String,
24}
25
26impl MetadataCache {
27    const CACHE_FILE: &'static str = "metadata_cache.json";
28
29    /// Load cache from the temp directory.
30    pub fn load(temp_dir: &Path) -> Option<Self> {
31        let cache_path = temp_dir.join(Self::CACHE_FILE);
32        let content = std::fs::read_to_string(&cache_path).ok()?;
33        serde_json::from_str(&content).ok()
34    }
35
36    /// Save cache to the temp directory.
37    pub fn save(&self, temp_dir: &Path) -> std::io::Result<()> {
38        let cache_path = temp_dir.join(Self::CACHE_FILE);
39        let content = serde_json::to_string_pretty(self)?;
40        std::fs::write(cache_path, content)
41    }
42
43    /// Compute hash of Cargo.lock file.
44    pub fn hash_cargo_lock(workspace_root: &Path) -> Option<String> {
45        let lock_path = workspace_root.join("Cargo.lock");
46        let content = std::fs::read(&lock_path).ok()?;
47        Some(blake3::hash(&content).to_hex().to_string())
48    }
49
50    /// Check if the Cargo.lock hash matches the cached one.
51    pub fn is_valid(&self, workspace_root: &Path) -> bool {
52        Self::hash_cargo_lock(workspace_root)
53            .map(|h| h == self.cargo_lock_hash)
54            .unwrap_or(false)
55    }
56}
57
58/// Compute blake3 hash of all .rs files in a source directory.
59///
60/// Used for staleness detection - saving a file without modifications
61/// won't change the hash, avoiding unnecessary rebuilds.
62pub fn compute_content_hash(src_dir: &Path) -> String {
63    use blake3::Hasher;
64
65    let mut hasher = Hasher::new();
66    let mut files: Vec<std::path::PathBuf> = Vec::new();
67
68    if src_dir.exists() {
69        let walker = walkdir::WalkDir::new(src_dir);
70        for entry in walker.into_iter().filter_map(|e| e.ok()) {
71            let path = entry.path();
72            if path.is_file() && path.extension().is_some_and(|e| e == "rs") {
73                files.push(path.to_path_buf());
74            }
75        }
76    }
77
78    // Sort for deterministic order
79    files.sort();
80
81    // Hash path + content for each file
82    for path in files {
83        if let Ok(content) = std::fs::read(&path) {
84            hasher.update(path.to_string_lossy().as_bytes());
85            hasher.update(&content);
86        }
87    }
88
89    hasher.finalize().to_hex().to_string()
90}
91
92/// Runner binary cache tracking which content hashes it was built with.
93///
94/// Stored at the workspace level since the runner is monolithic.
95#[derive(Debug, Default, Deserialize, Serialize)]
96pub struct RunnerCache {
97    /// Map of crate name -> content hash when runner was last built
98    pub crate_hashes: std::collections::HashMap<String, String>,
99    /// Mtime of runner binary when cache was created
100    pub runner_mtime: u64,
101    /// Version of es-fluent-cli that built this runner
102    /// Missing/mismatched version triggers rebuild to pick up helper changes
103    #[serde(default)]
104    pub cli_version: String,
105}
106
107impl RunnerCache {
108    const CACHE_FILE: &'static str = "runner_cache.json";
109
110    /// Load cache from the temp directory.
111    pub fn load(temp_dir: &Path) -> Option<Self> {
112        let cache_path = temp_dir.join(Self::CACHE_FILE);
113        let content = std::fs::read_to_string(&cache_path).ok()?;
114        serde_json::from_str(&content).ok()
115    }
116
117    /// Save cache to the temp directory.
118    pub fn save(&self, temp_dir: &Path) -> std::io::Result<()> {
119        let cache_path = temp_dir.join(Self::CACHE_FILE);
120        let content = serde_json::to_string_pretty(self)?;
121        std::fs::write(cache_path, content)
122    }
123}