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
159impl From<zvec_core::error::ZvecError> for Error {
160    fn from(value: zvec_core::error::ZvecError) -> Self {
161        use zvec_core::error::ZvecError;
162        let (code, message) = match value {
163            ZvecError::NotFound(message) => (ErrorCode::NotFound, message),
164            ZvecError::AlreadyExists(message) => (ErrorCode::AlreadyExists, message),
165            ZvecError::InvalidArgument(message) => (ErrorCode::InvalidArgument, message),
166            ZvecError::PermissionDenied(message) => (ErrorCode::PermissionDenied, message),
167            ZvecError::FailedPrecondition(message) => (ErrorCode::FailedPrecondition, message),
168            ZvecError::ResourceExhausted(message) => (ErrorCode::ResourceExhausted, message),
169            ZvecError::Unavailable(message) => (ErrorCode::Unavailable, message),
170            ZvecError::Internal(message) => (ErrorCode::InternalError, message),
171            ZvecError::NotSupported(message) => (ErrorCode::NotSupported, message),
172            ZvecError::Unknown(message) => (ErrorCode::Unknown, message),
173        };
174        Self::new(code, message)
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn status_codes_are_stable() {
184        assert_eq!(u32::from(ErrorCode::InvalidArgument), 3);
185        assert_eq!(ErrorCode::from(99), ErrorCode::Unknown);
186    }
187
188    #[test]
189    fn helpers_preserve_context() {
190        let error = Error::not_found("document x");
191        assert!(error.is_not_found());
192        assert!(error.to_string().contains("document x"));
193    }
194}