use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::content_hash::HashManifest;
const MANIFEST_FILE: &str = "hash_manifest.json";
#[derive(Debug, Clone)]
pub struct BuildCache {
cache_dir: PathBuf,
}
impl BuildCache {
#[must_use]
pub fn new(project_root: &Path) -> Self {
Self {
cache_dir: project_root.join(".bock").join("cache"),
}
}
#[must_use]
pub fn with_cache_dir(cache_dir: PathBuf) -> Self {
Self { cache_dir }
}
#[must_use]
pub fn cache_dir(&self) -> &Path {
&self.cache_dir
}
pub fn ensure_cache_dir(&self) -> io::Result<()> {
fs::create_dir_all(&self.cache_dir)
}
pub fn load_manifest(&self) -> Result<HashManifest, CacheError> {
let path = self.cache_dir.join(MANIFEST_FILE);
if !path.exists() {
return Ok(HashManifest::new());
}
let content = fs::read_to_string(&path).map_err(CacheError::Io)?;
serde_json::from_str(&content).map_err(CacheError::Parse)
}
pub fn save_manifest(&self, manifest: &HashManifest) -> Result<(), CacheError> {
self.ensure_cache_dir().map_err(CacheError::Io)?;
let path = self.cache_dir.join(MANIFEST_FILE);
let content = serde_json::to_string_pretty(manifest).map_err(CacheError::Serialize)?;
fs::write(&path, content).map_err(CacheError::Io)?;
Ok(())
}
pub fn clear(&self) -> io::Result<()> {
if self.cache_dir.exists() {
fs::remove_dir_all(&self.cache_dir)?;
}
Ok(())
}
#[must_use]
pub fn has_manifest(&self) -> bool {
self.cache_dir.join(MANIFEST_FILE).exists()
}
}
#[derive(Debug)]
pub enum CacheError {
Io(io::Error),
Parse(serde_json::Error),
Serialize(serde_json::Error),
}
impl std::fmt::Display for CacheError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "cache I/O error: {e}"),
Self::Parse(e) => write!(f, "cache parse error: {e}"),
Self::Serialize(e) => write!(f, "cache serialization error: {e}"),
}
}
}
impl std::error::Error for CacheError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::Parse(e) => Some(e),
Self::Serialize(e) => Some(e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::content_hash::ContentHash;
#[test]
fn cache_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let cache = BuildCache::new(dir.path());
let mut manifest = HashManifest::new();
manifest.insert("Main".to_string(), ContentHash::of_str("fn main() {}"));
manifest.insert("Lib".to_string(), ContentHash::of_str("fn helper() {}"));
cache.save_manifest(&manifest).unwrap();
assert!(cache.has_manifest());
let loaded = cache.load_manifest().unwrap();
assert_eq!(loaded.len(), 2);
assert_eq!(loaded.get("Main"), manifest.get("Main"));
assert_eq!(loaded.get("Lib"), manifest.get("Lib"));
}
#[test]
fn cache_empty_when_no_file() {
let dir = tempfile::tempdir().unwrap();
let cache = BuildCache::new(dir.path());
let manifest = cache.load_manifest().unwrap();
assert!(manifest.is_empty());
assert!(!cache.has_manifest());
}
#[test]
fn cache_clear() {
let dir = tempfile::tempdir().unwrap();
let cache = BuildCache::new(dir.path());
let manifest = HashManifest::new();
cache.save_manifest(&manifest).unwrap();
assert!(cache.has_manifest());
cache.clear().unwrap();
assert!(!cache.has_manifest());
assert!(!cache.cache_dir().exists());
}
#[test]
fn cache_dir_path() {
let cache = BuildCache::new(Path::new("/project"));
assert_eq!(cache.cache_dir(), Path::new("/project/.bock/cache"));
}
#[test]
fn cache_overwrite() {
let dir = tempfile::tempdir().unwrap();
let cache = BuildCache::new(dir.path());
let mut m1 = HashManifest::new();
m1.insert("A".to_string(), ContentHash::of_str("v1"));
cache.save_manifest(&m1).unwrap();
let mut m2 = HashManifest::new();
m2.insert("A".to_string(), ContentHash::of_str("v2"));
m2.insert("B".to_string(), ContentHash::of_str("v1"));
cache.save_manifest(&m2).unwrap();
let loaded = cache.load_manifest().unwrap();
assert_eq!(loaded.len(), 2);
assert_eq!(loaded.get("A"), m2.get("A"));
}
#[test]
fn ensure_cache_dir_creates_nested() {
let dir = tempfile::tempdir().unwrap();
let cache = BuildCache::new(dir.path());
assert!(!cache.cache_dir().exists());
cache.ensure_cache_dir().unwrap();
assert!(cache.cache_dir().exists());
}
}