//! 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. /// 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.rs`'s read-path /// tests; 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, } #[cfg(test)] mod tests { use super::Error; /// Each distinguished variant constructs (store-api.md §Error /// model: the five failure families, one enum). #[test] fn each_variant_constructs() { let variants: [Error; 5] = [ Error::Missing, Error::Verification, Error::Io("cursor closed mid-read".to_owned()), Error::GcRefuse, Error::KeyInvalid, ]; let expected = [ "key missing", "verification failed", "io: cursor closed mid-read", "direct delete refused", "invalid key bytes", ]; 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.rs read-path tests). #[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; 5] = [ Error::Missing, Error::Verification, Error::Io("media".to_owned()), Error::GcRefuse, Error::KeyInvalid, ]; assert_eq!(facade_surface.len(), 5); } /// `get`/`stat` signal absence with `None` and never carry /// `Missing` (ADR-012 §6.5 ruling 5; store-api.md §Error model) /// — enforced behaviorally by the facade's `get`/`stat` returning /// `Option` (the store.rs read-path tests pin the both-tier-miss /// shapes); this test is the doc contract's in-module marker. #[test] fn missing_is_exclusive_to_err_shaped_ops() { let absence_on_get: Option = None; assert_eq!(absence_on_get.map(|_| Error::Missing), None); } }