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}
50
51/// Library error type returned by most public APIs.
52#[derive(Debug, Clone)]
53pub struct MeowError {
54 /// Numeric error code, usually mapped from [`InnerErrorCode`].
55 code: i32,
56 /// Human-readable error message.
57 msg: String,
58 /// Optional chained source error.
59 source: Option<Arc<dyn StdError + Send + Sync>>,
60}
61
62impl MeowError {
63 /// Creates a new error with raw numeric code and message.
64 ///
65 /// # Examples
66 ///
67 /// ```no_run
68 /// use rusty_cat::api::MeowError;
69 ///
70 /// let err = MeowError::new(9999, "custom failure".to_string());
71 /// assert_eq!(err.code(), 9999);
72 /// ```
73 pub fn new(code: i32, msg: String) -> Self {
74 crate::log::emit_lazy(|| {
75 crate::log::Log::debug("error", format!("MeowError::new code={} msg={}", code, msg))
76 });
77 MeowError {
78 code,
79 msg,
80 source: None,
81 }
82 }
83
84 /// Returns numeric error code.
85 ///
86 /// # Examples
87 ///
88 /// ```no_run
89 /// use rusty_cat::api::{InnerErrorCode, MeowError};
90 ///
91 /// let err = MeowError::from_code1(InnerErrorCode::ClientClosed);
92 /// assert_eq!(err.code(), InnerErrorCode::ClientClosed as i32);
93 /// ```
94 pub fn code(&self) -> i32 {
95 self.code
96 }
97
98 /// Returns the error message as a borrowed `&str`.
99 ///
100 /// Borrowing avoids an allocation on every call; callers that need an
101 /// owned `String` can do `err.msg().to_owned()` explicitly.
102 ///
103 /// # Examples
104 ///
105 /// ```no_run
106 /// use rusty_cat::api::{InnerErrorCode, MeowError};
107 ///
108 /// let err = MeowError::from_code_str(InnerErrorCode::InvalidRange, "bad range");
109 /// assert_eq!(err.msg(), "bad range");
110 /// ```
111 pub fn msg(&self) -> &str {
112 &self.msg
113 }
114
115 /// Creates an error from [`InnerErrorCode`] with empty message.
116 ///
117 /// # Examples
118 ///
119 /// ```no_run
120 /// use rusty_cat::api::{InnerErrorCode, MeowError};
121 ///
122 /// let err = MeowError::from_code1(InnerErrorCode::ParameterEmpty);
123 /// assert_eq!(err.code(), InnerErrorCode::ParameterEmpty as i32);
124 /// ```
125 pub fn from_code1(code: InnerErrorCode) -> Self {
126 crate::log::emit_lazy(|| {
127 crate::log::Log::debug("error", format!("MeowError::from_code1 code={:?}", code))
128 });
129 MeowError {
130 code: code as i32,
131 msg: String::new(),
132 source: None,
133 }
134 }
135
136 /// Creates an error from [`InnerErrorCode`] and message.
137 ///
138 /// # Examples
139 ///
140 /// ```no_run
141 /// use rusty_cat::api::{InnerErrorCode, MeowError};
142 ///
143 /// let err = MeowError::from_code(InnerErrorCode::EnqueueError, "enqueue failed".to_string());
144 /// assert_eq!(err.code(), InnerErrorCode::EnqueueError as i32);
145 /// ```
146 pub fn from_code(code: InnerErrorCode, msg: String) -> Self {
147 crate::log::emit_lazy(|| {
148 crate::log::Log::debug(
149 "error",
150 format!("MeowError::from_code code={:?} msg={}", code, msg),
151 )
152 });
153 MeowError {
154 code: code as i32,
155 msg,
156 source: None,
157 }
158 }
159
160 /// Creates an error from [`InnerErrorCode`] and `&str` message.
161 ///
162 /// # Examples
163 ///
164 /// ```no_run
165 /// use rusty_cat::api::{InnerErrorCode, MeowError};
166 ///
167 /// let err = MeowError::from_code_str(InnerErrorCode::TaskNotFound, "unknown id");
168 /// assert_eq!(err.code(), InnerErrorCode::TaskNotFound as i32);
169 /// ```
170 pub fn from_code_str(code: InnerErrorCode, msg: &str) -> Self {
171 crate::log::emit_lazy(|| {
172 crate::log::Log::debug(
173 "error",
174 format!("MeowError::from_code_str code={:?} msg={}", code, msg),
175 )
176 });
177 MeowError {
178 code: code as i32,
179 msg: msg.to_string(),
180 source: None,
181 }
182 }
183
184 /// Creates an error with source chaining.
185 ///
186 /// Use this helper to preserve original low-level errors.
187 ///
188 /// # Examples
189 ///
190 /// ```no_run
191 /// use rusty_cat::api::{InnerErrorCode, MeowError};
192 ///
193 /// let source = std::io::Error::other("disk error");
194 /// let err = MeowError::from_source(InnerErrorCode::IoError, "upload failed", source);
195 /// assert_eq!(err.code(), InnerErrorCode::IoError as i32);
196 /// ```
197 pub fn from_source<E>(code: InnerErrorCode, msg: impl Into<String>, source: E) -> Self
198 where
199 E: StdError + Send + Sync + 'static,
200 {
201 let msg = msg.into();
202 let source_preview = source.to_string();
203 crate::log::emit_lazy(|| {
204 crate::log::Log::debug(
205 "error",
206 format!(
207 "MeowError::from_source code={:?} msg={} source={}",
208 code, msg, source_preview
209 ),
210 )
211 });
212 MeowError {
213 code: code as i32,
214 msg,
215 source: Some(Arc::new(source)),
216 }
217 }
218}
219
220impl PartialEq for MeowError {
221 fn eq(&self, other: &Self) -> bool {
222 self.code == other.code && self.msg == other.msg
223 }
224}
225
226impl Eq for MeowError {}
227
228impl Display for MeowError {
229 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
230 if self.msg.is_empty() {
231 write!(f, "MeowError(code={})", self.code)
232 } else {
233 write!(f, "MeowError(code={}, msg={})", self.code, self.msg)
234 }
235 }
236}
237
238impl StdError for MeowError {
239 fn source(&self) -> Option<&(dyn StdError + 'static)> {
240 self.source
241 .as_deref()
242 .map(|e| e as &(dyn StdError + 'static))
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::{InnerErrorCode, MeowError};
249
250 #[test]
251 fn meow_error_display_contains_code_and_message() {
252 let err = MeowError::from_code_str(InnerErrorCode::InvalidRange, "bad range");
253 let s = format!("{err}");
254 assert!(s.contains("code="));
255 assert!(s.contains("bad range"));
256 }
257
258 #[test]
259 fn meow_error_source_is_accessible() {
260 let io = std::io::Error::other("disk io");
261 let err = MeowError::from_source(InnerErrorCode::IoError, "io failed", io);
262 assert!(std::error::Error::source(&err).is_some());
263 }
264}