haz-cache 0.2.0

Content-addressed cache for haz task outputs using BLAKE3.
Documentation
//! The [`CacheReader`] handle: a read-only, filesystem-bound view
//! onto the cache tree at `<workspace-root>/.haz/cache` per
//! [`crate::layout`].
//!
//! The bound is the read-only [`Filesystem`] trait, NOT
//! `WritableFilesystem`. The compiler enforces what `AUX-018` /
//! `AUX-020` declare in prose: no method reachable through a
//! [`CacheReader`] can create, remove, rename, or modify any path
//! under the cache root, because the trait it speaks against does
//! not expose those operations.
//!
//! Read-only operations on cache state live in sibling modules:
//! [`crate::lookup`] for `lookup` / `lookup_status` (`CACHE-014`),
//! [`crate::info`] for `info` (`AUX-017..AUX-021`).

use std::path::{Path, PathBuf};

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

use crate::layout;

/// Read-only handle to the cache tree rooted at
/// `<workspace_root>/.haz/cache`, parameterised over a
/// [`Filesystem`] backend.
///
/// Pairs with [`crate::CacheWriter`]: a writer wraps a reader and
/// adds the mutating operations under the stricter
/// [`haz_vfs::WritableFilesystem`] bound. Consumers that only need
/// to consult the cache (e.g. `haz cache info` per `AUX-018`, the
/// cache-introspection step of `haz why` per `AUX-014`) take a
/// [`CacheReader`] and gain a compile-time guarantee that no write
/// can sneak in.
#[derive(Debug, Clone)]
pub struct CacheReader<Fs: Filesystem> {
    fs: Fs,
    workspace_root: PathBuf,
    root: PathBuf,
    hash_algo: HashAlgo,
}

impl<Fs: Filesystem> CacheReader<Fs> {
    /// Construct a [`CacheReader`] handle for the workspace rooted
    /// at `workspace_root`. The cache root is derived as
    /// `<workspace_root>/.haz/cache` via [`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 {
            fs,
            workspace_root: workspace_root.to_path_buf(),
            root: layout::cache_root(workspace_root),
            hash_algo,
        }
    }

    /// 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.workspace_root
    }

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

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

    /// Borrow the underlying filesystem handle.
    #[must_use]
    pub fn fs(&self) -> &Fs {
        &self.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::CacheReader;

    #[test]
    fn workspace_root_is_preserved() {
        let fs = MemFilesystem::new();
        let cache = CacheReader::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 = CacheReader::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 = CacheReader::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 = CacheReader::new(fs, Path::new("/ws"), HashAlgo::Blake3);
        cache.fs().metadata(Path::new("/ws")).unwrap();
    }
}