haz-cache 0.2.0

Content-addressed cache for haz task outputs using BLAKE3.
Documentation
//! The [`CacheWriter`] handle: a writable, filesystem-bound view
//! onto the cache tree at `<workspace-root>/.haz/cache` per
//! [`crate::layout`].
//!
//! Pairs with [`crate::CacheReader`]. The writer holds a reader as
//! its inner state; lookup, schema introspection, and any other
//! read-only operation are reached via [`CacheWriter::reader`]
//! rather than being re-exposed here. The split keeps the read-only
//! contract of `AUX-018` / `AUX-020` enforceable at the type level:
//! a future commit attempting to publish a write through a
//! read-only path no longer compiles, because the [`CacheReader`]
//! it would have to flow through does not implement
//! [`haz_vfs::WritableFilesystem`].
//!
//! Write-side operations live in sibling modules:
//! [`crate::store`] for [`CacheWriter::store`] (`CACHE-017`),
//! [`crate::restore`] for [`CacheWriter::restore`] (`CACHE-019`),
//! [`crate::clean`] for [`CacheWriter::clear`] / [`CacheWriter::clean`]
//! (`CACHE-021`, `CACHE-022`, `AUX-022..AUX-027`).

use std::path::Path;

use haz_domain::settings::cache::HashAlgo;
use haz_vfs::WritableFilesystem;

use crate::reader::CacheReader;

/// Writable handle to the cache tree rooted at
/// `<workspace_root>/.haz/cache`, parameterised over a
/// [`WritableFilesystem`] backend.
///
/// Wraps a [`CacheReader`]; reads against the same cache flow
/// through [`Self::reader`] so the read-only contract of the
/// reader's surface is preserved.
#[derive(Debug, Clone)]
pub struct CacheWriter<Fs: WritableFilesystem> {
    reader: CacheReader<Fs>,
}

impl<Fs: WritableFilesystem> CacheWriter<Fs> {
    /// Construct a [`CacheWriter`] handle for the workspace rooted
    /// at `workspace_root`. The cache root is derived as
    /// `<workspace_root>/.haz/cache` via
    /// [`crate::layout::cache_root`].
    ///
    /// `hash_algo` is the active hash function for this cache
    /// session and is used by lookup to reject entries that were
    /// written under a different algorithm (`CACHE-016` step 3).
    pub fn new(fs: Fs, workspace_root: &Path, hash_algo: HashAlgo) -> Self {
        Self {
            reader: CacheReader::new(fs, workspace_root, hash_algo),
        }
    }

    /// Borrow the inner [`CacheReader`] for read-only operations
    /// (lookup, manifest introspection, info).
    #[must_use]
    pub fn reader(&self) -> &CacheReader<Fs> {
        &self.reader
    }

    /// The absolute path to the workspace root. Restoration
    /// (`CACHE-019`) needs this to map workspace-anchored
    /// `/foo/bar` paths recorded in the manifest into real
    /// filesystem paths.
    #[must_use]
    pub fn workspace_root(&self) -> &Path {
        self.reader.workspace_root()
    }

    /// The absolute path to the cache root,
    /// `<workspace_root>/.haz/cache`.
    #[must_use]
    pub fn cache_root(&self) -> &Path {
        self.reader.cache_root()
    }

    /// The hash function this [`CacheWriter`] was constructed with.
    #[must_use]
    pub fn hash_algo(&self) -> HashAlgo {
        self.reader.hash_algo()
    }

    /// Borrow the underlying filesystem handle.
    #[must_use]
    pub fn fs(&self) -> &Fs {
        self.reader.fs()
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use haz_domain::settings::cache::HashAlgo;
    use haz_vfs::Filesystem;
    use haz_vfs_testing::MemFilesystem;

    use super::CacheWriter;

    #[test]
    fn workspace_root_is_preserved() {
        let fs = MemFilesystem::new();
        let cache = CacheWriter::new(fs, Path::new("/ws"), HashAlgo::Blake3);
        assert_eq!(cache.workspace_root(), Path::new("/ws"));
    }

    #[test]
    fn cache_010_cache_root_is_workspace_dot_haz_cache() {
        let fs = MemFilesystem::new();
        let cache = CacheWriter::new(fs, Path::new("/ws"), HashAlgo::Blake3);
        assert_eq!(cache.cache_root(), Path::new("/ws/.haz/cache"));
    }

    #[test]
    fn cache_002_hash_algo_is_preserved() {
        let fs = MemFilesystem::new();
        let cache = CacheWriter::new(fs, Path::new("/ws"), HashAlgo::Sha256);
        assert_eq!(cache.hash_algo(), HashAlgo::Sha256);
    }

    #[test]
    fn fs_accessor_returns_the_handle_passed_in() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/ws").unwrap();
        let cache = CacheWriter::new(fs, Path::new("/ws"), HashAlgo::Blake3);
        cache.fs().metadata(Path::new("/ws")).unwrap();
    }

    #[test]
    fn reader_accessor_borrows_an_inner_reader_with_the_same_state() {
        let fs = MemFilesystem::new();
        let cache = CacheWriter::new(fs, Path::new("/ws"), HashAlgo::Blake3);
        let reader = cache.reader();
        assert_eq!(reader.workspace_root(), Path::new("/ws"));
        assert_eq!(reader.cache_root(), Path::new("/ws/.haz/cache"));
        assert_eq!(reader.hash_algo(), HashAlgo::Blake3);
    }
}