Skip to main content

hasp_core/
error.rs

1use thiserror::Error;
2
3/// hasp library-surface errors. Stable across all backends; backend impls
4/// map their native error vocabulary into these variants.
5///
6/// This enum is intentionally flat so consumers can match on variants
7/// without string-matching nested SDK-style error hierarchies.
8#[non_exhaustive]
9#[derive(Debug, Error, Clone)]
10pub enum Error {
11    /// URL parse failure at the `url` crate layer (malformed URI).
12    #[error("invalid URL: {0}")]
13    UrlParse(#[from] url::ParseError),
14
15    /// URL parsed but does not satisfy a backend-specific grammar rule
16    /// (e.g. wrong path-segment count, missing required component).
17    #[error("invalid URL for backend: {0}")]
18    InvalidUrl(String),
19
20    /// URL scheme is not registered in this Store.
21    #[error("unsupported scheme: {0}")]
22    UnknownScheme(String),
23
24    /// Backend recognized the URL but does not implement the requested verb
25    /// (e.g. `env://` does not support `put`).
26    #[error("{scheme} does not support {operation}")]
27    UnsupportedOperation {
28        scheme: &'static str,
29        operation: &'static str,
30    },
31
32    /// The addressed secret does not exist.
33    #[error("not found: {0}")]
34    NotFound(String),
35
36    /// Caller's credentials are valid; caller is not authorized for this resource.
37    #[error("permission denied: {0}")]
38    PermissionDenied(String),
39
40    /// Caller's credentials are missing, invalid, or expired.
41    #[error("authentication failed: {0}")]
42    AuthenticationFailed(String),
43
44    /// Resource is in a state that blocks the operation, even though the caller
45    /// has permission and the resource exists.
46    #[error("precondition failed: {0}")]
47    PreconditionFailed(String),
48
49    /// Backend-specific failure that does not fit a more-specific variant.
50    /// `kind` provides retry guidance; `message` carries diagnostic detail
51    /// (backend error code, HTTP status, sub-error). Never includes secret values.
52    #[error("backend '{scheme}' failed: {message}")]
53    Backend {
54        scheme: &'static str,
55        kind: BackendFailureKind,
56        message: String,
57    },
58}
59
60/// Retry-policy classification for `Error::Backend`.
61#[non_exhaustive]
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum BackendFailureKind {
64    /// Temporary failure; retry with exponential backoff.
65    Transient,
66
67    /// Backend is rate-limiting the caller. Retry honoring any Retry-After
68    /// signal surfaced in `Backend.message`.
69    Throttled,
70
71    /// Permanent failure; retry will not help without external action.
72    Permanent,
73}
74
75impl Error {
76    /// True if a retry has any chance of succeeding without external action.
77    ///
78    /// Returns `true` for `Backend { kind: Transient | Throttled, .. }`.
79    /// Returns `false` for everything else, including `NotFound`,
80    /// `PermissionDenied`, `AuthenticationFailed`, `PreconditionFailed`.
81    pub fn is_transient(&self) -> bool {
82        matches!(
83            self,
84            Error::Backend {
85                kind: BackendFailureKind::Transient | BackendFailureKind::Throttled,
86                ..
87            }
88        )
89    }
90
91    /// Stable classifier for use in audit events and exit-code mapping.
92    ///
93    /// The returned label is closed-set — pattern-matchable by SIEMs
94    /// and scripts without parsing the human-readable message. Add a
95    /// new arm here only when adding a new `Error` variant.
96    #[allow(unreachable_patterns)]
97    pub fn kind(&self) -> &'static str {
98        match self {
99            Error::UrlParse(_) => "url_parse",
100            Error::InvalidUrl(_) => "invalid_url",
101            Error::UnknownScheme(_) => "unknown_scheme",
102            Error::UnsupportedOperation { .. } => "unsupported_operation",
103            Error::NotFound(_) => "not_found",
104            Error::PermissionDenied(_) => "permission_denied",
105            Error::AuthenticationFailed(_) => "auth_failed",
106            Error::PreconditionFailed(_) => "precondition_failed",
107            Error::Backend { .. } => "backend",
108            // `Error` is `#[non_exhaustive]`; new variants land here
109            // until the match arm is added above.
110            _ => "other",
111        }
112    }
113}