jacquardSnapshot

← snapshot

4930 bytes
//! The storage port and its in-memory fake.

use std::collections::BTreeMap;

use jac_core::{BlobId, SnapshotId, TreeId};
use thiserror::Error;

use crate::encode::{blob_id, snapshot_id, tree_id};
use crate::object::Blob;
use crate::snapshot::Snapshot;
use crate::tree::Tree;

/// Failures reading or writing the object store.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum ObjectError {
    /// The requested blob is not in the store.
    #[error("blob not found")]
    BlobNotFound,
    /// The requested tree is not in the store.
    #[error("tree not found")]
    TreeNotFound,
    /// The requested snapshot is not in the store.
    #[error("snapshot not found")]
    SnapshotNotFound,
    /// A stored object's bytes no longer hash to its id.
    #[error("stored object does not match its content address")]
    Corrupt,
}

/// Where objects live. Sans-IO: the core never touches disk or network; real
/// storage is an adapter implementing this trait outside the workspace.
pub trait ObjectStore {
    /// Stores a blob, returning its content address. Idempotent.
    fn put_blob(&mut self, blob: Blob) -> BlobId;

    /// Stores a tree, returning its content address. Idempotent.
    fn put_tree(&mut self, tree: Tree) -> TreeId;

    /// Stores a snapshot, returning its content address. Idempotent.
    fn put_snapshot(&mut self, snapshot: Snapshot) -> SnapshotId;

    /// Fetches a blob.
    ///
    /// # Errors
    ///
    /// Returns [`ObjectError::BlobNotFound`] if absent, or
    /// [`ObjectError::Corrupt`] if the stored bytes no longer match the id.
    fn blob(&self, id: BlobId) -> Result<&Blob, ObjectError>;

    /// Fetches a tree.
    ///
    /// # Errors
    ///
    /// Returns [`ObjectError::TreeNotFound`] if absent, or
    /// [`ObjectError::Corrupt`] on a content-address mismatch.
    fn tree(&self, id: TreeId) -> Result<&Tree, ObjectError>;

    /// Fetches a snapshot.
    ///
    /// # Errors
    ///
    /// Returns [`ObjectError::SnapshotNotFound`] if absent, or
    /// [`ObjectError::Corrupt`] on a content-address mismatch.
    fn snapshot(&self, id: SnapshotId) -> Result<&Snapshot, ObjectError>;
}

/// In-memory store for tests and the narrated demo.
///
/// Re-hashes every object on read. In a fake this is nearly free, and it
/// means any test that mutated stored state out from under an id fails
/// loudly as [`ObjectError::Corrupt`] instead of silently returning the
/// wrong content.
#[derive(Debug, Default)]
pub struct MemoryObjectStore {
    blobs: BTreeMap<BlobId, Blob>,
    trees: BTreeMap<TreeId, Tree>,
    snapshots: BTreeMap<SnapshotId, Snapshot>,
}

impl MemoryObjectStore {
    /// An empty store.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Number of objects held, for narration.
    #[must_use]
    pub fn object_count(&self) -> usize {
        self.blobs.len() + self.trees.len() + self.snapshots.len()
    }
}

impl ObjectStore for MemoryObjectStore {
    fn put_blob(&mut self, blob: Blob) -> BlobId {
        let id = blob_id(&blob);
        self.blobs.insert(id, blob);
        id
    }

    fn put_tree(&mut self, tree: Tree) -> TreeId {
        let id = tree_id(&tree);
        self.trees.insert(id, tree);
        id
    }

    fn put_snapshot(&mut self, snapshot: Snapshot) -> SnapshotId {
        let id = snapshot_id(&snapshot);
        self.snapshots.insert(id, snapshot);
        id
    }

    fn blob(&self, id: BlobId) -> Result<&Blob, ObjectError> {
        let blob = self.blobs.get(&id).ok_or(ObjectError::BlobNotFound)?;
        if blob_id(blob) == id {
            Ok(blob)
        } else {
            Err(ObjectError::Corrupt)
        }
    }

    fn tree(&self, id: TreeId) -> Result<&Tree, ObjectError> {
        let tree = self.trees.get(&id).ok_or(ObjectError::TreeNotFound)?;
        if tree_id(tree) == id {
            Ok(tree)
        } else {
            Err(ObjectError::Corrupt)
        }
    }

    fn snapshot(&self, id: SnapshotId) -> Result<&Snapshot, ObjectError> {
        let snapshot = self
            .snapshots
            .get(&id)
            .ok_or(ObjectError::SnapshotNotFound)?;
        if snapshot_id(snapshot) == id {
            Ok(snapshot)
        } else {
            Err(ObjectError::Corrupt)
        }
    }
}

#[cfg(test)]
#[expect(
    clippy::unwrap_used,
    reason = "test assertions read better with unwrap"
)]
mod tests {
    use super::*;

    #[test]
    fn put_then_get_roundtrips() {
        let mut store = MemoryObjectStore::new();
        let blob = Blob::new(&b"content"[..]);
        let id = store.put_blob(blob.clone());
        assert_eq!(store.blob(id).unwrap(), &blob);
    }

    #[test]
    fn missing_objects_are_not_found() {
        let store = MemoryObjectStore::new();
        let id = blob_id(&Blob::new(&b"never stored"[..]));
        assert_eq!(store.blob(id), Err(ObjectError::BlobNotFound));
    }
}