es_fluent_cli/generation/
cache.rs1use indexmap::IndexMap;
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10
11#[derive(Debug, Default, Deserialize, Serialize)]
16pub struct MetadataCache {
17 pub cargo_lock_hash: String,
19 pub es_fluent_dep: String,
21 pub es_fluent_cli_helpers_dep: String,
23 pub target_dir: String,
25}
26
27impl MetadataCache {
28 const CACHE_FILE: &'static str = "metadata_cache.json";
29
30 pub fn load(temp_dir: &Path) -> Option<Self> {
32 let cache_path = temp_dir.join(Self::CACHE_FILE);
33 let content = std::fs::read_to_string(&cache_path).ok()?;
34 serde_json::from_str(&content).ok()
35 }
36
37 pub fn save(&self, temp_dir: &Path) -> std::io::Result<()> {
39 let cache_path = temp_dir.join(Self::CACHE_FILE);
40 let content = serde_json::to_string_pretty(self)?;
41 std::fs::write(cache_path, content)
42 }
43
44 pub fn hash_cargo_lock(workspace_root: &Path) -> Option<String> {
46 let lock_path = workspace_root.join("Cargo.lock");
47 let content = std::fs::read(&lock_path).ok()?;
48 Some(blake3::hash(&content).to_hex().to_string())
49 }
50
51 pub fn is_valid(&self, workspace_root: &Path) -> bool {
53 Self::hash_cargo_lock(workspace_root)
54 .map(|h| h == self.cargo_lock_hash)
55 .unwrap_or(false)
56 }
57}
58
59pub fn compute_content_hash(src_dir: &Path, i18n_toml_path: Option<&Path>) -> String {
67 use blake3::Hasher;
68
69 let mut hasher = Hasher::new();
70 let mut files: Vec<std::path::PathBuf> = Vec::new();
71
72 if src_dir.exists() {
73 let walker = walkdir::WalkDir::new(src_dir);
74 for entry in walker.into_iter().filter_map(|e| e.ok()) {
75 let path = entry.path();
76 if path.is_file() && path.extension().is_some_and(|e| e == "rs") {
77 files.push(path.to_path_buf());
78 }
79 }
80 }
81
82 files.sort();
84
85 for path in files {
87 if let Ok(content) = std::fs::read(&path) {
88 hasher.update(path.to_string_lossy().as_bytes());
89 hasher.update(&content);
90 }
91 }
92
93 if let Some(toml_path) = i18n_toml_path
95 && toml_path.is_file()
96 && let Ok(content) = std::fs::read(toml_path)
97 {
98 hasher.update(toml_path.to_string_lossy().as_bytes());
99 hasher.update(&content);
100 }
101
102 hasher.finalize().to_hex().to_string()
103}
104
105#[derive(Debug, Default, Deserialize, Serialize)]
109pub struct RunnerCache {
110 pub crate_hashes: IndexMap<String, String>,
112 pub runner_mtime: u64,
114 #[serde(default)]
117 pub cli_version: String,
118}
119
120impl RunnerCache {
121 const CACHE_FILE: &'static str = "runner_cache.json";
122
123 pub fn load(temp_dir: &Path) -> Option<Self> {
125 let cache_path = temp_dir.join(Self::CACHE_FILE);
126 let content = std::fs::read_to_string(&cache_path).ok()?;
127 serde_json::from_str(&content).ok()
128 }
129
130 pub fn save(&self, temp_dir: &Path) -> std::io::Result<()> {
132 let cache_path = temp_dir.join(Self::CACHE_FILE);
133 let content = serde_json::to_string_pretty(self)?;
134 std::fs::write(cache_path, content)
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use std::fs;
142
143 #[test]
144 fn test_compute_content_hash_without_i18n_toml() {
145 let temp_dir = tempfile::tempdir().unwrap();
146 let src_dir = temp_dir.path().join("src");
147 fs::create_dir_all(&src_dir).unwrap();
148 fs::write(src_dir.join("lib.rs"), "fn main() {}").unwrap();
149
150 let hash1 = compute_content_hash(&src_dir, None);
151 let hash2 = compute_content_hash(&src_dir, None);
152
153 assert_eq!(hash1, hash2);
155 assert!(!hash1.is_empty());
156 }
157
158 #[test]
159 fn test_compute_content_hash_with_i18n_toml() {
160 let temp_dir = tempfile::tempdir().unwrap();
161 let src_dir = temp_dir.path().join("src");
162 fs::create_dir_all(&src_dir).unwrap();
163 fs::write(src_dir.join("lib.rs"), "fn main() {}").unwrap();
164
165 let i18n_path = temp_dir.path().join("i18n.toml");
166 fs::write(&i18n_path, "default_language = \"en\"").unwrap();
167
168 let hash_with_toml = compute_content_hash(&src_dir, Some(&i18n_path));
169 let hash_without_toml = compute_content_hash(&src_dir, None);
170
171 assert_ne!(hash_with_toml, hash_without_toml);
173 }
174
175 #[test]
176 fn test_compute_content_hash_changes_when_i18n_toml_changes() {
177 let temp_dir = tempfile::tempdir().unwrap();
178 let src_dir = temp_dir.path().join("src");
179 fs::create_dir_all(&src_dir).unwrap();
180 fs::write(src_dir.join("lib.rs"), "fn main() {}").unwrap();
181
182 let i18n_path = temp_dir.path().join("i18n.toml");
183 fs::write(&i18n_path, "default_language = \"en\"").unwrap();
184
185 let hash1 = compute_content_hash(&src_dir, Some(&i18n_path));
186
187 fs::write(
189 &i18n_path,
190 "default_language = \"en\"\nfluent_feature = \"i18n\"",
191 )
192 .unwrap();
193
194 let hash2 = compute_content_hash(&src_dir, Some(&i18n_path));
195
196 assert_ne!(hash1, hash2);
198 }
199
200 #[test]
201 fn test_compute_content_hash_unchanged_when_rs_unchanged() {
202 let temp_dir = tempfile::tempdir().unwrap();
203 let src_dir = temp_dir.path().join("src");
204 fs::create_dir_all(&src_dir).unwrap();
205 fs::write(src_dir.join("lib.rs"), "fn main() {}").unwrap();
206
207 let i18n_path = temp_dir.path().join("i18n.toml");
208 fs::write(&i18n_path, "default_language = \"en\"").unwrap();
209
210 let hash1 = compute_content_hash(&src_dir, Some(&i18n_path));
211
212 fs::write(src_dir.join("lib.rs"), "fn main() {}").unwrap();
214 fs::write(&i18n_path, "default_language = \"en\"").unwrap();
215
216 let hash2 = compute_content_hash(&src_dir, Some(&i18n_path));
217
218 assert_eq!(hash1, hash2);
220 }
221
222 #[test]
223 fn test_compute_content_hash_nonexistent_i18n_toml() {
224 let temp_dir = tempfile::tempdir().unwrap();
225 let src_dir = temp_dir.path().join("src");
226 fs::create_dir_all(&src_dir).unwrap();
227 fs::write(src_dir.join("lib.rs"), "fn main() {}").unwrap();
228
229 let nonexistent_path = temp_dir.path().join("nonexistent.toml");
230
231 let hash_with_nonexistent = compute_content_hash(&src_dir, Some(&nonexistent_path));
233 let hash_without = compute_content_hash(&src_dir, None);
234
235 assert_eq!(hash_with_nonexistent, hash_without);
236 }
237
238 #[test]
239 fn test_compute_content_hash_only_rs_files() {
240 let temp_dir = tempfile::tempdir().unwrap();
241 let src_dir = temp_dir.path().join("src");
242 fs::create_dir_all(&src_dir).unwrap();
243 fs::write(src_dir.join("lib.rs"), "fn main() {}").unwrap();
244
245 let hash1 = compute_content_hash(&src_dir, None);
246
247 fs::write(src_dir.join("notes.txt"), "some notes").unwrap();
249
250 let hash2 = compute_content_hash(&src_dir, None);
251
252 assert_eq!(hash1, hash2);
253 }
254}