//! 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));
}
}