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}