Skip to main content

a3s_vec/
error.rs

1//! Error types used by `a3s-vec`.
2//!
3//! The public error intentionally mirrors zvec's status taxonomy while still
4//! carrying a useful, typed Rust error.  Keeping the status code at the API
5//! boundary makes it possible for adapters (CLI, HTTP, and A3S Code) to map
6//! failures without parsing strings.
7
8use std::fmt;
9
10/// Stable status categories exposed by the collection API.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[repr(u32)]
13pub enum ErrorCode {
14    NotFound = 1,
15    AlreadyExists = 2,
16    InvalidArgument = 3,
17    PermissionDenied = 4,
18    FailedPrecondition = 5,
19    ResourceExhausted = 6,
20    Unavailable = 7,
21    InternalError = 8,
22    NotSupported = 9,
23    Unknown = 10,
24}
25
26impl From<u32> for ErrorCode {
27    fn from(value: u32) -> Self {
28        match value {
29            1 => Self::NotFound,
30            2 => Self::AlreadyExists,
31            3 => Self::InvalidArgument,
32            4 => Self::PermissionDenied,
33            5 => Self::FailedPrecondition,
34            6 => Self::ResourceExhausted,
35            7 => Self::Unavailable,
36            8 => Self::InternalError,
37            9 => Self::NotSupported,
38            _ => Self::Unknown,
39        }
40    }
41}
42
43impl From<ErrorCode> for u32 {
44    fn from(value: ErrorCode) -> Self {
45        value as u32
46    }
47}
48
49impl fmt::Display for ErrorCode {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        let name = match self {
52            Self::NotFound => "NotFound",
53            Self::AlreadyExists => "AlreadyExists",
54            Self::InvalidArgument => "InvalidArgument",
55            Self::PermissionDenied => "PermissionDenied",
56            Self::FailedPrecondition => "FailedPrecondition",
57            Self::ResourceExhausted => "ResourceExhausted",
58            Self::Unavailable => "Unavailable",
59            Self::InternalError => "InternalError",
60            Self::NotSupported => "NotSupported",
61            Self::Unknown => "Unknown",
62        };
63        f.write_str(name)
64    }
65}
66
67/// An error returned by a3s-vec.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct Error {
70    /// Stable machine-readable category.
71    pub code: ErrorCode,
72    /// Human-readable context.  Messages never contain a secret by design.
73    pub message: String,
74}
75
76impl Error {
77    /// Creates an error with a stable code and context.
78    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
79        Self {
80            code,
81            message: message.into(),
82        }
83    }
84
85    pub fn not_found(message: impl Into<String>) -> Self {
86        Self::new(ErrorCode::NotFound, message)
87    }
88
89    pub fn already_exists(message: impl Into<String>) -> Self {
90        Self::new(ErrorCode::AlreadyExists, message)
91    }
92
93    pub fn invalid_argument(message: impl Into<String>) -> Self {
94        Self::new(ErrorCode::InvalidArgument, message)
95    }
96
97    pub fn permission_denied(message: impl Into<String>) -> Self {
98        Self::new(ErrorCode::PermissionDenied, message)
99    }
100
101    pub fn failed_precondition(message: impl Into<String>) -> Self {
102        Self::new(ErrorCode::FailedPrecondition, message)
103    }
104
105    pub fn internal(message: impl Into<String>) -> Self {
106        Self::new(ErrorCode::InternalError, message)
107    }
108
109    pub fn resource_exhausted(message: impl Into<String>) -> Self {
110        Self::new(ErrorCode::ResourceExhausted, message)
111    }
112
113    pub fn not_supported(message: impl Into<String>) -> Self {
114        Self::new(ErrorCode::NotSupported, message)
115    }
116
117    pub fn is_not_found(&self) -> bool {
118        self.code == ErrorCode::NotFound
119    }
120
121    pub fn is_already_exists(&self) -> bool {
122        self.code == ErrorCode::AlreadyExists
123    }
124
125    pub fn is_invalid_argument(&self) -> bool {
126        self.code == ErrorCode::InvalidArgument
127    }
128}
129
130impl fmt::Display for Error {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "a3s-vec error {}: {}", self.code, self.message)
133    }
134}
135
136impl std::error::Error for Error {}
137
138/// Specialized result type for all public operations.
139pub type Result<T> = std::result::Result<T, Error>;
140
141impl From<std::io::Error> for Error {
142    fn from(value: std::io::Error) -> Self {
143        let code = match value.kind() {
144            std::io::ErrorKind::NotFound => ErrorCode::NotFound,
145            std::io::ErrorKind::PermissionDenied => ErrorCode::PermissionDenied,
146            std::io::ErrorKind::AlreadyExists => ErrorCode::AlreadyExists,
147            _ => ErrorCode::InternalError,
148        };
149        Self::new(code, value.to_string())
150    }
151}
152
153impl From<serde_json::Error> for Error {
154    fn from(value: serde_json::Error) -> Self {
155        Self::new(ErrorCode::InternalError, format!("JSON error: {value}"))
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn status_codes_are_stable() {
165        for code in [
166            ErrorCode::NotFound,
167            ErrorCode::AlreadyExists,
168            ErrorCode::InvalidArgument,
169            ErrorCode::PermissionDenied,
170            ErrorCode::FailedPrecondition,
171            ErrorCode::ResourceExhausted,
172            ErrorCode::Unavailable,
173            ErrorCode::InternalError,
174            ErrorCode::NotSupported,
175            ErrorCode::Unknown,
176        ] {
177            assert_eq!(ErrorCode::from(u32::from(code)), code);
178            assert!(!format!("{code}").is_empty());
179        }
180        assert_eq!(u32::from(ErrorCode::InvalidArgument), 3);
181        assert_eq!(ErrorCode::from(99), ErrorCode::Unknown);
182    }
183
184    #[test]
185    fn helpers_preserve_context() {
186        let error = Error::not_found("document x");
187        assert!(error.is_not_found());
188        assert!(error.to_string().contains("document x"));
189        assert_eq!(Error::already_exists("dup").code, ErrorCode::AlreadyExists);
190        assert_eq!(
191            Error::invalid_argument("bad").code,
192            ErrorCode::InvalidArgument
193        );
194        assert_eq!(
195            Error::permission_denied("no").code,
196            ErrorCode::PermissionDenied
197        );
198        assert_eq!(
199            Error::failed_precondition("pre").code,
200            ErrorCode::FailedPrecondition
201        );
202        assert_eq!(
203            Error::resource_exhausted("oom").code,
204            ErrorCode::ResourceExhausted
205        );
206        assert_eq!(Error::internal("boom").code, ErrorCode::InternalError);
207        assert_eq!(Error::not_supported("yet").code, ErrorCode::NotSupported);
208        assert_eq!(
209            Error::new(ErrorCode::Unavailable, "down").code,
210            ErrorCode::Unavailable
211        );
212    }
213
214    #[test]
215    fn json_errors_map_to_internal() {
216        let err: Error = serde_json::from_str::<serde_json::Value>("{")
217            .unwrap_err()
218            .into();
219        assert_eq!(err.code, ErrorCode::InternalError);
220        assert!(err.message.contains("JSON"));
221    }
222
223    #[test]
224    fn io_and_predicate_helpers_cover_surface() {
225        let not_found: Error = std::io::Error::new(std::io::ErrorKind::NotFound, "gone").into();
226        assert!(not_found.is_not_found());
227        let exists: Error = std::io::Error::new(std::io::ErrorKind::AlreadyExists, "here").into();
228        assert!(exists.is_already_exists());
229        let denied: Error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "no").into();
230        assert_eq!(denied.code, ErrorCode::PermissionDenied);
231        let other: Error = std::io::Error::other("boom").into();
232        assert_eq!(other.code, ErrorCode::InternalError);
233        assert!(Error::invalid_argument("x").is_invalid_argument());
234        assert!(!Error::not_found("y").is_invalid_argument());
235    }
236}