rusty_cat/error.rs
1use std::error::Error as StdError;
2use std::fmt::{Display, Formatter};
3use std::sync::Arc;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum InnerErrorCode {
7 /// Unknown/unclassified error.
8 Unknown = -1,
9 /// Success (non-error sentinel).
10 Success = 0,
11 /// Runtime creation failed.
12 RuntimeCreationFailedError = 101,
13 /// Required parameter is empty or invalid.
14 ParameterEmpty = 102,
15 /// The same file/task is already queued or running.
16 DuplicateTaskError = 103,
17 /// Failed to enqueue task.
18 EnqueueError = 104,
19 /// Local I/O operation failed.
20 IoError = 105,
21 /// HTTP request/response operation failed.
22 HttpError = 106,
23 /// Client has already been closed and can no longer accept operations.
24 ClientClosed = 107,
25 /// Unknown task ID in control API.
26 TaskNotFound = 108,
27 /// HTTP response status is not expected.
28 ResponseStatusError = 109,
29 /// `Content-Length` from HEAD is missing or invalid.
30 MissingOrInvalidContentLengthFromHead = 110,
31 /// Failed to send command to scheduler thread.
32 CommandSendFailed = 111,
33 /// Command response channel closed unexpectedly.
34 CommandResponseFailed = 112,
35 /// Failed to parse response payload (for example JSON).
36 ResponseParseError = 113,
37 /// Invalid HTTP range semantics or headers.
38 InvalidRange = 114,
39 /// Local file does not exist.
40 FileNotFound = 115,
41 /// File checksum/signature does not match expected value.
42 ChecksumMismatch = 116,
43 /// Current task state does not allow requested operation.
44 InvalidTaskState = 117,
45 /// Internal lock is poisoned.
46 LockPoisoned = 118,
47 /// Failed to build internal HTTP client.
48 HttpClientBuildFailed = 119,
49 /// Task was canceled before reaching `Complete`.
50 TaskCanceled = 120,
51}
52
53/// Library error type returned by most public APIs.
54#[derive(Debug, Clone)]
55pub struct MeowError {
56 /// Numeric error code, usually mapped from [`InnerErrorCode`].
57 code: i32,
58 /// Human-readable error message.
59 msg: String,
60 /// Optional chained source error.
61 source: Option<Arc<dyn StdError + Send + Sync>>,
62}
63
64impl MeowError {
65 /// Creates a new error with raw numeric code and message.
66 ///
67 /// # Examples
68 ///
69 /// ```no_run
70 /// use rusty_cat::api::MeowError;
71 ///
72 /// let err = MeowError::new(9999, "custom failure".to_string());
73 /// assert_eq!(err.code(), 9999);
74 /// ```
75 pub fn new(code: i32, msg: String) -> Self {
76 crate::log::emit_lazy(|| {
77 crate::log::Log::debug("error", format!("MeowError::new code={} msg={}", code, msg))
78 });
79 MeowError {
80 code,
81 msg,
82 source: None,
83 }
84 }
85
86 /// Returns numeric error code.
87 ///
88 /// # Examples
89 ///
90 /// ```no_run
91 /// use rusty_cat::api::{InnerErrorCode, MeowError};
92 ///
93 /// let err = MeowError::from_code1(InnerErrorCode::ClientClosed);
94 /// assert_eq!(err.code(), InnerErrorCode::ClientClosed as i32);
95 /// ```
96 pub fn code(&self) -> i32 {
97 self.code
98 }
99
100 /// Returns the error message as a borrowed `&str`.
101 ///
102 /// Borrowing avoids an allocation on every call; callers that need an
103 /// owned `String` can do `err.msg().to_owned()` explicitly.
104 ///
105 /// # Examples
106 ///
107 /// ```no_run
108 /// use rusty_cat::api::{InnerErrorCode, MeowError};
109 ///
110 /// let err = MeowError::from_code_str(InnerErrorCode::InvalidRange, "bad range");
111 /// assert_eq!(err.msg(), "bad range");
112 /// ```
113 pub fn msg(&self) -> &str {
114 &self.msg
115 }
116
117 /// Creates an error from [`InnerErrorCode`] with empty message.
118 ///
119 /// # Examples
120 ///
121 /// ```no_run
122 /// use rusty_cat::api::{InnerErrorCode, MeowError};
123 ///
124 /// let err = MeowError::from_code1(InnerErrorCode::ParameterEmpty);
125 /// assert_eq!(err.code(), InnerErrorCode::ParameterEmpty as i32);
126 /// ```
127 pub fn from_code1(code: InnerErrorCode) -> Self {
128 crate::log::emit_lazy(|| {
129 crate::log::Log::debug("error", format!("MeowError::from_code1 code={:?}", code))
130 });
131 MeowError {
132 code: code as i32,
133 msg: String::new(),
134 source: None,
135 }
136 }
137
138 /// Creates an error from [`InnerErrorCode`] and message.
139 ///
140 /// # Examples
141 ///
142 /// ```no_run
143 /// use rusty_cat::api::{InnerErrorCode, MeowError};
144 ///
145 /// let err = MeowError::from_code(InnerErrorCode::EnqueueError, "enqueue failed".to_string());
146 /// assert_eq!(err.code(), InnerErrorCode::EnqueueError as i32);
147 /// ```
148 pub fn from_code(code: InnerErrorCode, msg: String) -> Self {
149 crate::log::emit_lazy(|| {
150 crate::log::Log::debug(
151 "error",
152 format!("MeowError::from_code code={:?} msg={}", code, msg),
153 )
154 });
155 MeowError {
156 code: code as i32,
157 msg,
158 source: None,
159 }
160 }
161
162 /// Creates an error from [`InnerErrorCode`] and `&str` message.
163 ///
164 /// # Examples
165 ///
166 /// ```no_run
167 /// use rusty_cat::api::{InnerErrorCode, MeowError};
168 ///
169 /// let err = MeowError::from_code_str(InnerErrorCode::TaskNotFound, "unknown id");
170 /// assert_eq!(err.code(), InnerErrorCode::TaskNotFound as i32);
171 /// ```
172 pub fn from_code_str(code: InnerErrorCode, msg: &str) -> Self {
173 crate::log::emit_lazy(|| {
174 crate::log::Log::debug(
175 "error",
176 format!("MeowError::from_code_str code={:?} msg={}", code, msg),
177 )
178 });
179 MeowError {
180 code: code as i32,
181 msg: msg.to_string(),
182 source: None,
183 }
184 }
185
186 /// Creates an error with source chaining.
187 ///
188 /// Use this helper to preserve original low-level errors.
189 ///
190 /// # Examples
191 ///
192 /// ```no_run
193 /// use rusty_cat::api::{InnerErrorCode, MeowError};
194 ///
195 /// let source = std::io::Error::other("disk error");
196 /// let err = MeowError::from_source(InnerErrorCode::IoError, "upload failed", source);
197 /// assert_eq!(err.code(), InnerErrorCode::IoError as i32);
198 /// ```
199 pub fn from_source<E>(code: InnerErrorCode, msg: impl Into<String>, source: E) -> Self
200 where
201 E: StdError + Send + Sync + 'static,
202 {
203 let msg = msg.into();
204 let source_preview = source.to_string();
205 crate::log::emit_lazy(|| {
206 crate::log::Log::debug(
207 "error",
208 format!(
209 "MeowError::from_source code={:?} msg={} source={}",
210 code, msg, source_preview
211 ),
212 )
213 });
214 MeowError {
215 code: code as i32,
216 msg,
217 source: Some(Arc::new(source)),
218 }
219 }
220}
221
222impl PartialEq for MeowError {
223 fn eq(&self, other: &Self) -> bool {
224 self.code == other.code && self.msg == other.msg
225 }
226}
227
228impl Eq for MeowError {}
229
230impl Display for MeowError {
231 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
232 if self.msg.is_empty() {
233 write!(f, "MeowError(code={})", self.code)
234 } else {
235 write!(f, "MeowError(code={}, msg={})", self.code, self.msg)
236 }
237 }
238}
239
240impl StdError for MeowError {
241 fn source(&self) -> Option<&(dyn StdError + 'static)> {
242 self.source
243 .as_deref()
244 .map(|e| e as &(dyn StdError + 'static))
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::{InnerErrorCode, MeowError};
251
252 #[test]
253 fn meow_error_display_contains_code_and_message() {
254 let err = MeowError::from_code_str(InnerErrorCode::InvalidRange, "bad range");
255 let s = format!("{err}");
256 assert!(s.contains("code="));
257 assert!(s.contains("bad range"));
258 }
259
260 #[test]
261 fn meow_error_source_is_accessible() {
262 let io = std::io::Error::other("disk io");
263 let err = MeowError::from_source(InnerErrorCode::IoError, "io failed", io);
264 assert!(std::error::Error::source(&err).is_some());
265 }
266}