alkblobs 0.1.0

Content-addressed blob storage: multi-hash put/get/verify store with pluggable backends, transport- and protocol-agnostic
Documentation
//! The error family (store-api.md §Error model; ADR-012 §6.5, §1).
//!
//! **One enum, both altitudes.** This is a *single* enum serving both
//! altitudes of the crate's surface. The [`Backend`][crate::backend]
//! trait's methods return this family but its *effective* members are
//! `Error::Missing`, `Error::Verification`, `Error::Io`, and
//! `Error::KeyInvalid` — at the
//! trait boundary, keys arrive opaque (so `Error::KeyInvalid` never fires
//! there) and GC delete-arbitration answers flow up as data, not as
//! errors (so `Error::GcRefuse` never fires below the facade). The facade's
//! public surface re-exposes this same family with `Error::GcRefuse` added:
//! the facade decides and owns the refusal. The altitude discipline is
//! carried by doc comments and construction discipline, not by two
//! separate types (named design point audited at the review gate;
//! store-api.md §Error model).
//!
//! The variant set is ADR-pinned (ADR-012 §6.5's family statement);
//! new failure families are new ADRs, so `#[non_exhaustive]` is
//! deliberately not applied — ADR-019 exercised that rule, adding
//! `RegistryFull` as the refusal family the token registry's
//! admission cap answers with.

/// The store error family — one enum with members at two altitudes
/// (store-api.md §Error model; ADR-012 §6.5, §1).
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub enum Error {
    /// Key absent, on ops that signal absence via `Err`:
    /// `read_range` and `delete` (ADR-012 §6.5 ruling 5;
    /// store-api.md §Error model).
    ///
    /// A miss on `get` and `stat` is a value, not an error — they
    /// signal absence with `None` and never carry `Missing`. The
    /// facade's read paths enforce this behaviorally: both-tier misses
    /// resolve `Ok(None)` on get/stat (see `src/store/read.rs` and the
    /// read-path tests pinning the miss shapes — `src/store/tests.rs`,
    /// `tests/conformance/rows.rs`; ADR-012 §6.5 ruling 5;
    /// store-api.md §Error model).
    #[error("key missing (read_range/delete: get/stat signal absence with None)")]
    Missing,

    /// Put/get hash-check failure: content does not match the key
    /// under the canonical derivation (store-api.md §Error model).
    #[error("verification failed: content does not match key")]
    Verification,

    /// Backend media failure (store-api.md §Error model), covering
    /// cursor and staged-put I/O errors surfaced through the same
    /// trait error family — stringly because `std::io::Error` is not
    /// stable across versions.
    #[error("io: {0}")]
    Io(String),

    /// Direct delete refused: the key is protected — pinned,
    /// registered-source-live, or protect-callback-named — and
    /// nothing was deleted (ADR-005's delete-window arbitration at
    /// delete time; ADR-012 §1/§4).
    ///
    /// Never fires below the facade: arbitration answers flow up as
    /// data, and the facade decides and owns the refusal (one enum,
    /// both altitudes; store-api.md §Error model). Named `GcRefuse`
    /// per ADR-012 §1 — there is no sweep-abort error.
    #[error("direct delete refused: key protected (nothing deleted)")]
    GcRefuse,

    /// Malformed key bytes at the boundary — truncated, unknown
    /// algorithm (ADR-002/hashing): rejects, never guesses
    /// (store-api.md §Error model). KeyInvalid never fires at the
    /// [`Backend`][crate::backend] trait boundary, where keys arrive
    /// opaque (one enum, both altitudes).
    #[error("invalid key bytes: truncated, unknown algorithm — rejects, never guesses")]
    KeyInvalid,

    /// The pin-token registry's admission refusal (ADR-019): a mint
    /// at or past the registry's admission cap — no token issued, no
    /// insertion (never an eviction), a custodial guard dropped, no
    /// custody taken. Ops-layer only (the registry is ops-layer
    /// state): it never fires below the facade, and like `GcRefuse`
    /// it is refusal-shaped — nothing happened, here is why.
    #[error("token registry at its admission cap: mint refused, no token issued")]
    RegistryFull,
}

#[cfg(test)]
mod tests {
    use super::Error;

    /// Each distinguished variant constructs (store-api.md §Error
    /// model: the failure families, one enum — five at ADR-012 §6.5,
    /// the sixth (`RegistryFull`) at ADR-019).
    #[test]
    fn each_variant_constructs() {
        let variants: [Error; 6] = [
            Error::Missing,
            Error::Verification,
            Error::Io("cursor closed mid-read".to_owned()),
            Error::GcRefuse,
            Error::KeyInvalid,
            Error::RegistryFull,
        ];
        let expected = [
            "key missing",
            "verification failed",
            "io: cursor closed mid-read",
            "direct delete refused",
            "invalid key bytes",
            "admission cap",
        ];
        for (err, text) in variants.iter().zip(expected) {
            assert!(err.to_string().contains(text), "{err:?}");
        }
    }

    /// The named design point (store-api.md §Error model): one enum,
    /// both altitudes — the trait boundary's effective members plus
    /// the facade re-exposure with `GcRefuse` added, carried by doc
    /// comments and construction discipline rather than two types.
    /// Facade behavioral enforcement is in place on the read paths
    /// (`get`/`stat` never carry `Missing` — their both-tier misses
    /// are `Ok(None)`, pinned by the store/ read-path and conformance
    /// tests). ADR-019's `RegistryFull` rides the same enum as the
    /// ops-layer member — neither a trait nor a facade member (the
    /// registry is ops-layer state; it never fires at either
    /// boundary).
    #[test]
    fn one_enum_both_altitudes() {
        let trait_effective: [Error; 4] = [
            Error::Missing,
            Error::Verification,
            Error::Io("media".to_owned()),
            Error::KeyInvalid,
        ];
        assert_eq!(trait_effective.len(), 4);
        let facade_surface: [Error; 6] = [
            Error::Missing,
            Error::Verification,
            Error::Io("media".to_owned()),
            Error::GcRefuse,
            Error::KeyInvalid,
            Error::RegistryFull,
        ];
        assert_eq!(facade_surface.len(), 6);
    }
}