1use std::fs;
8use std::io;
9use std::path::{Path, PathBuf};
10
11use crate::content_hash::HashManifest;
12
13const MANIFEST_FILE: &str = "hash_manifest.json";
15
16#[derive(Debug, Clone)]
18pub struct BuildCache {
19 cache_dir: PathBuf,
21}
22
23impl BuildCache {
24 #[must_use]
29 pub fn new(project_root: &Path) -> Self {
30 Self {
31 cache_dir: project_root.join(".bock").join("cache"),
32 }
33 }
34
35 #[must_use]
37 pub fn with_cache_dir(cache_dir: PathBuf) -> Self {
38 Self { cache_dir }
39 }
40
41 #[must_use]
43 pub fn cache_dir(&self) -> &Path {
44 &self.cache_dir
45 }
46
47 pub fn ensure_cache_dir(&self) -> io::Result<()> {
53 fs::create_dir_all(&self.cache_dir)
54 }
55
56 pub fn load_manifest(&self) -> Result<HashManifest, CacheError> {
64 let path = self.cache_dir.join(MANIFEST_FILE);
65 if !path.exists() {
66 return Ok(HashManifest::new());
67 }
68
69 let content = fs::read_to_string(&path).map_err(CacheError::Io)?;
70 serde_json::from_str(&content).map_err(CacheError::Parse)
71 }
72
73 pub fn save_manifest(&self, manifest: &HashManifest) -> Result<(), CacheError> {
81 self.ensure_cache_dir().map_err(CacheError::Io)?;
82
83 let path = self.cache_dir.join(MANIFEST_FILE);
84 let content = serde_json::to_string_pretty(manifest).map_err(CacheError::Serialize)?;
85 fs::write(&path, content).map_err(CacheError::Io)?;
86
87 Ok(())
88 }
89
90 pub fn clear(&self) -> io::Result<()> {
96 if self.cache_dir.exists() {
97 fs::remove_dir_all(&self.cache_dir)?;
98 }
99 Ok(())
100 }
101
102 #[must_use]
104 pub fn has_manifest(&self) -> bool {
105 self.cache_dir.join(MANIFEST_FILE).exists()
106 }
107}
108
109#[derive(Debug)]
111pub enum CacheError {
112 Io(io::Error),
114 Parse(serde_json::Error),
116 Serialize(serde_json::Error),
118}
119
120impl std::fmt::Display for CacheError {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 match self {
123 Self::Io(e) => write!(f, "cache I/O error: {e}"),
124 Self::Parse(e) => write!(f, "cache parse error: {e}"),
125 Self::Serialize(e) => write!(f, "cache serialization error: {e}"),
126 }
127 }
128}
129
130impl std::error::Error for CacheError {
131 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
132 match self {
133 Self::Io(e) => Some(e),
134 Self::Parse(e) => Some(e),
135 Self::Serialize(e) => Some(e),
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::content_hash::ContentHash;
144
145 #[test]
146 fn cache_roundtrip() {
147 let dir = tempfile::tempdir().unwrap();
148 let cache = BuildCache::new(dir.path());
149
150 let mut manifest = HashManifest::new();
151 manifest.insert("Main".to_string(), ContentHash::of_str("fn main() {}"));
152 manifest.insert("Lib".to_string(), ContentHash::of_str("fn helper() {}"));
153
154 cache.save_manifest(&manifest).unwrap();
155 assert!(cache.has_manifest());
156
157 let loaded = cache.load_manifest().unwrap();
158 assert_eq!(loaded.len(), 2);
159 assert_eq!(loaded.get("Main"), manifest.get("Main"));
160 assert_eq!(loaded.get("Lib"), manifest.get("Lib"));
161 }
162
163 #[test]
164 fn cache_empty_when_no_file() {
165 let dir = tempfile::tempdir().unwrap();
166 let cache = BuildCache::new(dir.path());
167
168 let manifest = cache.load_manifest().unwrap();
169 assert!(manifest.is_empty());
170 assert!(!cache.has_manifest());
171 }
172
173 #[test]
174 fn cache_clear() {
175 let dir = tempfile::tempdir().unwrap();
176 let cache = BuildCache::new(dir.path());
177
178 let manifest = HashManifest::new();
179 cache.save_manifest(&manifest).unwrap();
180 assert!(cache.has_manifest());
181
182 cache.clear().unwrap();
183 assert!(!cache.has_manifest());
184 assert!(!cache.cache_dir().exists());
185 }
186
187 #[test]
188 fn cache_dir_path() {
189 let cache = BuildCache::new(Path::new("/project"));
190 assert_eq!(cache.cache_dir(), Path::new("/project/.bock/cache"));
191 }
192
193 #[test]
194 fn cache_overwrite() {
195 let dir = tempfile::tempdir().unwrap();
196 let cache = BuildCache::new(dir.path());
197
198 let mut m1 = HashManifest::new();
199 m1.insert("A".to_string(), ContentHash::of_str("v1"));
200 cache.save_manifest(&m1).unwrap();
201
202 let mut m2 = HashManifest::new();
203 m2.insert("A".to_string(), ContentHash::of_str("v2"));
204 m2.insert("B".to_string(), ContentHash::of_str("v1"));
205 cache.save_manifest(&m2).unwrap();
206
207 let loaded = cache.load_manifest().unwrap();
208 assert_eq!(loaded.len(), 2);
209 assert_eq!(loaded.get("A"), m2.get("A"));
210 }
211
212 #[test]
213 fn ensure_cache_dir_creates_nested() {
214 let dir = tempfile::tempdir().unwrap();
215 let cache = BuildCache::new(dir.path());
216
217 assert!(!cache.cache_dir().exists());
218 cache.ensure_cache_dir().unwrap();
219 assert!(cache.cache_dir().exists());
220 }
221}