turnframe_store/error.rs
1//! The error surface of the persistence contract.
2//!
3//! Every store speaks [`StoreError`] from `turnframe-core` and nothing else, so
4//! the runtime classifies persistence failures uniformly (see
5//! `turnframe_core::error::ErrorClassification`). The variants and the meaning
6//! this crate attaches to them:
7//!
8//! | Variant | Meaning in this contract |
9//! |---|---|
10//! | `NotFound` | The addressed record does not exist **for this account**. An id that belongs to another tenant produces exactly the same error (spec §25.4). |
11//! | `Conflict` | A uniqueness rule or a compare-and-swap precondition failed: a second blocking interaction on a case, a status transition from the wrong state, a duplicate identifier, an outbox key already enqueued. |
12//! | `Unavailable` | The backend could not be reached; nothing was written. Retryable. |
13//! | `Timeout` | The backend did not answer in time; the write **may** have landed. Callers must re-read, never blindly retry (spec §16.5). |
14//! | `Serialization` | A payload could not be encoded or decoded at the storage boundary. |
15//! | `Corrupt` | Stored data violates an invariant the adapter relies on (including a poisoned lock in the in-memory store). |
16//! | `Other { code }` | A refusal identified by a stable code from [`codes`]. |
17//!
18//! `Display` output of every variant is safe to log: identifiers and codes only.
19
20pub use turnframe_core::error::StoreError;
21
22/// Shorthand for the result of a store operation.
23pub type StoreResult<T> = Result<T, StoreError>;
24
25/// Stable codes carried by [`StoreError::Other`] when this contract refuses a
26/// request before touching storage.
27pub mod codes {
28 /// A record disagrees with the record it addresses: for example an assistant
29 /// turn whose `conversation_id` differs from the persisted user turn's, or a
30 /// user turn whose actor account differs from the addressed account.
31 pub const IDENTITY_MISMATCH: &str = "turnframe.store.identity_mismatch";
32 /// A record violates a structural precondition of the operation: an
33 /// interaction inserted in a status other than `Active`, an outbox entry
34 /// enqueued in a status other than `Pending`, an empty event batch.
35 pub const INVALID_RECORD: &str = "turnframe.store.invalid_record";
36 /// A commit bundle carries an item that belongs to another account than the
37 /// one the bundle is committed for.
38 pub const BUNDLE_ACCOUNT_MISMATCH: &str = "turnframe.store.bundle_account_mismatch";
39}
40
41/// Builds the [`codes::IDENTITY_MISMATCH`] refusal.
42#[must_use]
43pub fn identity_mismatch() -> StoreError {
44 StoreError::Other {
45 code: codes::IDENTITY_MISMATCH.to_owned(),
46 }
47}
48
49/// Builds the [`codes::INVALID_RECORD`] refusal.
50#[must_use]
51pub fn invalid_record() -> StoreError {
52 StoreError::Other {
53 code: codes::INVALID_RECORD.to_owned(),
54 }
55}
56
57/// Builds the [`codes::BUNDLE_ACCOUNT_MISMATCH`] refusal.
58#[must_use]
59pub fn bundle_account_mismatch() -> StoreError {
60 StoreError::Other {
61 code: codes::BUNDLE_ACCOUNT_MISMATCH.to_owned(),
62 }
63}
64
65/// Returns `true` when `error` is [`StoreError::Other`] with exactly `code`.
66#[must_use]
67pub fn has_code(error: &StoreError, code: &str) -> bool {
68 matches!(error, StoreError::Other { code: actual } if actual == code)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn code_helpers_round_trip() {
77 assert!(has_code(&identity_mismatch(), codes::IDENTITY_MISMATCH));
78 assert!(has_code(&invalid_record(), codes::INVALID_RECORD));
79 assert!(has_code(
80 &bundle_account_mismatch(),
81 codes::BUNDLE_ACCOUNT_MISMATCH
82 ));
83 assert!(!has_code(&StoreError::NotFound, codes::INVALID_RECORD));
84 assert!(!has_code(&invalid_record(), codes::IDENTITY_MISMATCH));
85 }
86
87 #[test]
88 fn display_carries_codes_only() {
89 assert_eq!(
90 invalid_record().to_string(),
91 "store failure turnframe.store.invalid_record"
92 );
93 }
94}