Skip to main content

waitprims_core/
error.rs

1//! Shared error type for waitprims-core.
2//!
3//! Validation [`Display`] reports a field path and constraint only. Raw
4//! input values are omitted.
5
6use thiserror::Error;
7
8use crate::jcs::JcsError;
9
10/// Machine-stable reason for a normative or set-rule failure.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum NormativeReason {
13    /// `run_deadline` is after `logical_deadline`.
14    DeadlineOrdering,
15    /// Claimed `registration_digest` does not match RFC 8785 SHA-256.
16    RegistrationDigestMismatch,
17    /// `no_change` invariants do not hold.
18    NoChangeInvariants,
19    /// `logical_deadman` invariants do not hold.
20    DeadmanInvariants,
21    /// Required-arm outage, uncertainty, or degradation used as a clean outcome.
22    OutageNotClean,
23    /// A complete outcome is missing a required arm.
24    CoverageCardinality,
25    /// Ack committed another registration's cursor or event ids.
26    CrossArmCommit,
27    /// Ack advanced past unretained events or cursors.
28    AckPastUnretained,
29    /// A required arm stayed deferred while fairness never rotated.
30    FairnessStarvation,
31    /// Cursor advanced across unacked events.
32    SilentCursorAdvance,
33    /// A wait request cited a different registration revision.
34    RevisionCross,
35    /// `authn_mode=required` without a verification receipt on a clean outcome.
36    AuthnRequired,
37    /// Wait completed after `lease_expires_at` without reauthentication.
38    LeaseReauth,
39    /// Per-registration event bound exceeded on a non-degraded outcome.
40    RegistrationBound,
41    /// Aggregate event bound exceeded on a non-degraded outcome.
42    AggregateBound,
43    /// A timestamp is outside the fail-closed RFC3339 profile.
44    UnparseableTimestamp,
45}
46
47impl NormativeReason {
48    /// Stable snake_case code used by the pin's control battery.
49    pub fn as_str(self) -> &'static str {
50        match self {
51            Self::DeadlineOrdering => "deadline_ordering",
52            Self::RegistrationDigestMismatch => "registration_digest_mismatch",
53            Self::NoChangeInvariants => "no_change_invariants",
54            Self::DeadmanInvariants => "deadman_invariants",
55            Self::OutageNotClean => "outage_not_clean",
56            Self::CoverageCardinality => "coverage_cardinality",
57            Self::CrossArmCommit => "cross_arm_commit",
58            Self::AckPastUnretained => "ack_past_unretained",
59            Self::FairnessStarvation => "fairness_starvation",
60            Self::SilentCursorAdvance => "silent_cursor_advance",
61            Self::RevisionCross => "revision_cross",
62            Self::AuthnRequired => "authn_required",
63            Self::LeaseReauth => "lease_reauth",
64            Self::RegistrationBound => "registration_bound",
65            Self::AggregateBound => "aggregate_bound",
66            Self::UnparseableTimestamp => "unparseable_timestamp",
67        }
68    }
69}
70
71impl std::fmt::Display for NormativeReason {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.write_str(self.as_str())
74    }
75}
76
77/// Validation failure: field path plus constraint. No raw input values.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ValidationError {
80    /// JSON pointer-style path to the failing field or document.
81    pub path: String,
82    /// Constraint that was not satisfied.
83    pub constraint: String,
84    /// Present when a named normative or set rule failed.
85    pub reason: Option<NormativeReason>,
86}
87
88impl ValidationError {
89    /// Construct a path + constraint error with no raw values.
90    pub fn new(path: impl Into<String>, constraint: impl Into<String>) -> Self {
91        Self {
92            path: path.into(),
93            constraint: constraint.into(),
94            reason: None,
95        }
96    }
97
98    /// Construct a named normative failure.
99    pub fn normative(
100        path: impl Into<String>,
101        constraint: impl Into<String>,
102        reason: NormativeReason,
103    ) -> Self {
104        Self {
105            path: path.into(),
106            constraint: constraint.into(),
107            reason: Some(reason),
108        }
109    }
110}
111
112impl std::fmt::Display for ValidationError {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(f, "{}: {}", self.path, self.constraint)
115    }
116}
117
118impl std::error::Error for ValidationError {}
119
120/// Canonical library error.
121#[derive(Debug, Error)]
122pub enum Error {
123    /// Contract pin resolution failed.
124    #[error("contract resolution failed at {path}: {constraint}")]
125    Contract {
126        /// Path of the missing or mismatched pin artifact.
127        path: &'static str,
128        /// Constraint that was not satisfied.
129        constraint: &'static str,
130    },
131    /// Message or set validation failed.
132    #[error(transparent)]
133    Validation(#[from] ValidationError),
134    /// RFC 8785 canonicalization failure.
135    #[error(transparent)]
136    Jcs(#[from] JcsError),
137    /// Input is not JSON.
138    #[error("malformed JSON")]
139    MalformedJson,
140}
141
142/// Result alias using [`Error`].
143pub type Result<T> = std::result::Result<T, Error>;
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn validation_display_omits_raw_values() {
151        let err = ValidationError::new("/message_type", "undeclared_message_type");
152        let shown = err.to_string();
153        assert!(shown.contains("/message_type"));
154        assert!(shown.contains("undeclared_message_type"));
155        assert!(!shown.contains("live_wait_ack"));
156        assert!(!shown.contains("secret"));
157    }
158}