Skip to main content

vibe_ready/api/
engine_error.rs

1use crate::log::log_def::CODE_STR;
2use crate::log_e;
3use crate::store::db::sql_def::DbError;
4use log::error;
5use std::backtrace::Backtrace;
6use std::fmt;
7
8#[repr(i32)]
9#[derive(Clone, Copy, Debug, PartialEq)]
10/// Stable numeric error codes returned by vibe-ready.
11pub enum VibeEngineErrorCode {
12    /// Unknown or unmapped error.
13    Unknown = -1,
14
15    /// Operation completed successfully.
16    Success = 200,
17
18    /// Engine has already been dropped.
19    EngineDropped = 100001,
20
21    /// Database has not been opened yet.
22    DatabaseNotOpened = 100002,
23
24    /// Database open operation failed.
25    DatabaseOpenFailed = 100003,
26    /// Log database open operation failed.
27    LogDatabaseOpenFailed = 100223,
28
29    /// Work database open operation failed.
30    WorkDatabaseOpenFailed = 132003,
31
32    /// Database I/O operation failed.
33    DatabaseIOError = 100004,
34
35    /// Requested database target was not found.
36    DatabaseTargetNotFound = 100005,
37
38    /// Database worker thread failed.
39    DatabaseThreadError = 100006,
40
41    /// Task could not be posted to the runtime.
42    PostError = 100007,
43    /// Task was cancelled before it could complete (B9 scheduler).
44    Cancelled = 100008,
45
46    /// Required parameter is empty.
47    ParameterEmpty = 100012,
48    /// OAuth flow failed.
49    OAuthError = 100013,
50    /// Engine configuration is invalid.
51    ConfigError = 100014,
52
53    /// Standard I/O error.
54    IOError = 100017,
55    /// HTTP or protocol bad-request error.
56    BadRequest = 100018,
57    /// Request operation failed.
58    RequestError = 100019,
59    /// Remote service returned an internal server error.
60    InternalServerError = 100020,
61    /// Network operation failed.
62    NetworkError = 100021,
63    /// Requested operation is unsupported.
64    UnsupportedError = 100022,
65
66    /// Operation timed out.
67    TimeoutError = 100023,
68    /// Connection attempt failed.
69    ConnectError = 100024,
70    /// TLS connection attempt failed.
71    TlsConnectError = 100025,
72    /// Options parsing failed.
73    OptionsParseError = 100026,
74
75    /// Deserialization failed.
76    SerdeDeserializeError = 100027,
77    /// Serialization failed.
78    SerdeSerializeError = 100028,
79
80    /// Log information argument is invalid.
81    InvalidArgumentLogInfo = 100029,
82
83    /// MPSC channel send failed.
84    MPSCSendError = 100037,
85
86    /// QR generation failed.
87    GenerateQRError = 10005,
88    /// Runtime operation failed.
89    RuntimeError = 10006,
90    /// Internal beaver subsystem failed.
91    BeaverError = 10007,
92    /// Socket receive operation timed out.
93    SocketRecvTimeout = 10008,
94    /// Socket has been closed.
95    SocketClosed = 10009,
96    /// Protocol parsing failed.
97    ProtocParseError = 10010,
98    /// Socket has not been opened.
99    SocketNotOpened = 10011,
100    /// Task was interrupted.
101    TaskInterruptionError = 10012,
102
103    /// Connection is closed.
104    ConnectionClosed = 30001,
105
106    /// Connection already exists.
107    ConnectionExists = 34001,
108
109    /// Connection is currently closing.
110    ConnectionClosing = 30027,
111
112    /// Internal operation failed.
113    InternalError = 32002,
114    /// User is not logged in.
115    NotLoggedInError = 32003,
116    /// Page token is invalid.
117    PageTokenError = 32004,
118    /// Clipboard initialization failed.
119    ClipboardInitializeError = 32005,
120
121    /// Feature or backend support has not been implemented yet.
122    NotSupportedYet = 999999,
123}
124
125impl VibeEngineErrorCode {
126    /// Returns the stable numeric code associated with this variant.
127    ///
128    /// # Returns
129    ///
130    /// The `i32` code used in serialized errors and logs.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use vibe_ready::VibeErrorCode;
136    ///
137    /// assert_eq!(VibeErrorCode::Success.code(), 200);
138    /// ```
139    pub fn code(&self) -> i32 {
140        *self as i32
141    }
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
145/// High-level category for a [`VibeEngineError`].
146pub enum VibeErrorKind {
147    /// Configuration or argument error.
148    Config,
149    /// Runtime or executor error.
150    Runtime,
151    /// Task scheduling or cancellation error.
152    Task,
153    /// Database or storage error.
154    Database,
155    /// Network or connection error.
156    Network,
157    /// Logging backend error.
158    Log,
159    /// Unsupported operation error.
160    Unsupported,
161    /// Internal SDK error.
162    Internal,
163    /// Unknown or uncategorized error.
164    Unknown,
165}
166
167impl VibeErrorKind {
168    fn from_code(code: VibeEngineErrorCode) -> Self {
169        match code {
170            VibeEngineErrorCode::ConfigError
171            | VibeEngineErrorCode::ParameterEmpty
172            | VibeEngineErrorCode::InvalidArgumentLogInfo => Self::Config,
173            VibeEngineErrorCode::RuntimeError
174            | VibeEngineErrorCode::EngineDropped
175            | VibeEngineErrorCode::MPSCSendError => Self::Runtime,
176            VibeEngineErrorCode::PostError
177            | VibeEngineErrorCode::TaskInterruptionError
178            | VibeEngineErrorCode::Cancelled => Self::Task,
179            VibeEngineErrorCode::DatabaseNotOpened
180            | VibeEngineErrorCode::DatabaseOpenFailed
181            | VibeEngineErrorCode::LogDatabaseOpenFailed
182            | VibeEngineErrorCode::WorkDatabaseOpenFailed
183            | VibeEngineErrorCode::DatabaseIOError
184            | VibeEngineErrorCode::DatabaseTargetNotFound
185            | VibeEngineErrorCode::DatabaseThreadError => Self::Database,
186            VibeEngineErrorCode::NetworkError
187            | VibeEngineErrorCode::BadRequest
188            | VibeEngineErrorCode::RequestError
189            | VibeEngineErrorCode::InternalServerError
190            | VibeEngineErrorCode::TimeoutError
191            | VibeEngineErrorCode::ConnectError
192            | VibeEngineErrorCode::TlsConnectError
193            | VibeEngineErrorCode::SocketRecvTimeout
194            | VibeEngineErrorCode::SocketClosed
195            | VibeEngineErrorCode::SocketNotOpened
196            | VibeEngineErrorCode::ConnectionClosed
197            | VibeEngineErrorCode::ConnectionExists
198            | VibeEngineErrorCode::ConnectionClosing => Self::Network,
199            VibeEngineErrorCode::UnsupportedError | VibeEngineErrorCode::NotSupportedYet => {
200                Self::Unsupported
201            }
202            VibeEngineErrorCode::InternalError
203            | VibeEngineErrorCode::IOError
204            | VibeEngineErrorCode::SerdeDeserializeError
205            | VibeEngineErrorCode::SerdeSerializeError
206            | VibeEngineErrorCode::OptionsParseError
207            | VibeEngineErrorCode::GenerateQRError
208            | VibeEngineErrorCode::BeaverError
209            | VibeEngineErrorCode::ProtocParseError
210            | VibeEngineErrorCode::OAuthError
211            | VibeEngineErrorCode::NotLoggedInError
212            | VibeEngineErrorCode::PageTokenError
213            | VibeEngineErrorCode::ClipboardInitializeError => Self::Internal,
214            VibeEngineErrorCode::Success | VibeEngineErrorCode::Unknown => Self::Unknown,
215        }
216    }
217}
218
219#[derive(Debug, Clone)]
220/// Error returned by vibe-ready operations.
221pub struct VibeEngineError {
222    code: i32,
223    kind: VibeErrorKind,
224    message: String,
225    source: Option<String>,
226    context: Vec<String>,
227}
228
229impl fmt::Display for VibeEngineError {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        write!(
232            f,
233            "vibe-ready error [{:?}/{}]: {}",
234            self.kind, self.code, self.message
235        )?;
236        if let Some(source) = &self.source {
237            write!(f, "; source: {source}")?;
238        }
239        if !self.context.is_empty() {
240            write!(f, "; context: {}", self.context.join("; "))?;
241        }
242        Ok(())
243    }
244}
245
246impl std::error::Error for VibeEngineError {}
247
248impl From<DbError> for VibeEngineError {
249    fn from(err: DbError) -> Self {
250        error!("DbError: {:?}, {:?}", err, Backtrace::capture());
251        let db_error = err.clone();
252        let mut error = match err {
253            DbError::OpenFailed => {
254                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseOpenFailed)
255            }
256            DbError::DatabaseIOError => {
257                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseIOError)
258            }
259            DbError::NotOpen => VibeEngineError::from_code(VibeEngineErrorCode::DatabaseOpenFailed),
260            DbError::TargetNotFound => {
261                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseTargetNotFound)
262            }
263            DbError::DatabaseThreadError => {
264                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseThreadError)
265            }
266            DbError::DatabaseUnlockError => {
267                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseOpenFailed)
268            }
269            DbError::NotSupportedYet => {
270                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseIOError)
271            }
272            DbError::JoinError => VibeEngineError::from_code(VibeEngineErrorCode::DatabaseIOError),
273            _ => VibeEngineError::from_code(VibeEngineErrorCode::Unknown),
274        };
275        error.source = Some(format!("{db_error:?}"));
276        error
277    }
278}
279
280impl VibeEngineError {
281    /// Returns the numeric error code.
282    ///
283    /// # Returns
284    ///
285    /// The stable `i32` code associated with this error.
286    pub fn code(&self) -> i32 {
287        self.code
288    }
289
290    /// Returns the high-level error category.
291    ///
292    /// # Returns
293    ///
294    /// A [`VibeErrorKind`] derived from the numeric code.
295    pub fn kind(&self) -> VibeErrorKind {
296        self.kind
297    }
298
299    /// Returns the human-readable error message.
300    ///
301    /// # Returns
302    ///
303    /// A borrowed message string.
304    pub fn message(&self) -> &str {
305        &self.message
306    }
307
308    /// Returns the optional source message.
309    ///
310    /// # Returns
311    ///
312    /// `Some(&str)` when a lower-level source was attached.
313    pub fn source_message(&self) -> Option<&str> {
314        self.source.as_deref()
315    }
316
317    /// Returns contextual breadcrumbs attached to the error.
318    ///
319    /// # Returns
320    ///
321    /// A borrowed slice of context strings.
322    pub fn context(&self) -> &[String] {
323        &self.context
324    }
325
326    /// Attaches a source message to this error.
327    ///
328    /// # Returns
329    ///
330    /// The updated error value.
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// use vibe_ready::{VibeEngineError, VibeErrorCode};
336    ///
337    /// let error = VibeEngineError::from_error_code(VibeErrorCode::RuntimeError)
338    ///     .with_source("worker stopped");
339    /// assert_eq!(error.source_message(), Some("worker stopped"));
340    /// ```
341    pub fn with_source(mut self, source: impl Into<String>) -> Self {
342        self.source = Some(source.into());
343        self
344    }
345
346    /// Adds contextual information to this error.
347    ///
348    /// # Returns
349    ///
350    /// The updated error value.
351    pub fn with_context(mut self, context: impl Into<String>) -> Self {
352        self.context.push(context.into());
353        self
354    }
355
356    /// Creates the canonical success value.
357    ///
358    /// # Returns
359    ///
360    /// A [`VibeEngineError`] whose code is [`VibeEngineErrorCode::Success`].
361    pub fn from_success() -> Self {
362        Self::from_error_code(VibeEngineErrorCode::Success)
363    }
364
365    /// Creates an error from an unsigned 16-bit raw code.
366    ///
367    /// # Returns
368    ///
369    /// A raw-code error with [`VibeErrorKind::Unknown`].
370    pub fn from_u16(code: u16) -> Self {
371        VibeEngineError::from_raw_code(code as i32)
372    }
373
374    /// Creates an error from an unsigned 32-bit raw code.
375    ///
376    /// # Returns
377    ///
378    /// A raw-code error with [`VibeErrorKind::Unknown`].
379    pub fn from_u32(code: u32) -> Self {
380        VibeEngineError::from_raw_code(code as i32)
381    }
382
383    /// Checks whether this error represents success.
384    ///
385    /// # Returns
386    ///
387    /// `true` when the code is [`VibeEngineErrorCode::Success`].
388    pub fn is_success(&self) -> bool {
389        self.code == VibeEngineErrorCode::Success.code()
390    }
391
392    /// Clones this error while preserving the same code and context.
393    ///
394    /// # Returns
395    ///
396    /// A clone of this error.
397    pub fn same_code(&self) -> Self {
398        self.clone()
399    }
400
401    /// Creates an error from a stable code variant.
402    ///
403    /// # Returns
404    ///
405    /// A [`VibeEngineError`] with default kind and message for the code.
406    pub fn from_error_code(value: VibeEngineErrorCode) -> Self {
407        VibeEngineError {
408            code: value.code(),
409            kind: VibeErrorKind::from_code(value),
410            message: default_message(value).to_string(),
411            source: None,
412            context: Vec::new(),
413        }
414    }
415
416    /// Creates an error from a stable code variant.
417    ///
418    /// # Returns
419    ///
420    /// A [`VibeEngineError`] with default kind and message for the code.
421    pub fn from_code(value: VibeEngineErrorCode) -> Self {
422        Self::from_error_code(value)
423    }
424
425    /// Creates an error from a code variant and custom message.
426    ///
427    /// # Returns
428    ///
429    /// A [`VibeEngineError`] using `msg` as the display message.
430    pub fn from_error_code_msg(value: VibeEngineErrorCode, msg: String) -> Self {
431        VibeEngineError {
432            message: msg,
433            ..Self::from_error_code(value)
434        }
435    }
436
437    /// Creates the standard task-interruption error.
438    ///
439    /// # Returns
440    ///
441    /// A [`VibeEngineError`] with [`VibeEngineErrorCode::TaskInterruptionError`].
442    pub fn from_task_interruption_error() -> Self {
443        Self::from_error_code(VibeEngineErrorCode::TaskInterruptionError)
444    }
445
446    /// Creates the standard internal error.
447    ///
448    /// # Returns
449    ///
450    /// A [`VibeEngineError`] with [`VibeEngineErrorCode::InternalError`].
451    pub fn from_internal_error() -> Self {
452        Self::from_error_code(VibeEngineErrorCode::InternalError)
453    }
454
455    /// Creates the standard MPSC send error.
456    ///
457    /// # Returns
458    ///
459    /// A [`VibeEngineError`] with [`VibeEngineErrorCode::MPSCSendError`].
460    pub fn from_mpsc_send_error() -> Self {
461        Self::from_error_code(VibeEngineErrorCode::MPSCSendError)
462    }
463
464    /// Creates the standard empty-parameter error.
465    ///
466    /// # Returns
467    ///
468    /// A [`VibeEngineError`] with [`VibeEngineErrorCode::ParameterEmpty`].
469    pub fn from_parameter_empty() -> Self {
470        Self::from_error_code(VibeEngineErrorCode::ParameterEmpty)
471    }
472    /// Logs and creates the standard empty-parameter error.
473    ///
474    /// # Returns
475    ///
476    /// A [`VibeEngineError`] with [`VibeEngineErrorCode::ParameterEmpty`].
477    pub fn from_parameter_empty_log(tag: &str) -> Self {
478        let code = VibeEngineErrorCode::ParameterEmpty.code();
479        log_e!(tag, CODE_STR, code);
480        Self::from_error_code(VibeEngineErrorCode::ParameterEmpty)
481    }
482
483    fn from_raw_code(code: i32) -> Self {
484        VibeEngineError {
485            code,
486            kind: VibeErrorKind::Unknown,
487            message: format!("unknown error code {code}"),
488            source: None,
489            context: Vec::new(),
490        }
491    }
492}
493
494fn default_message(code: VibeEngineErrorCode) -> &'static str {
495    match code {
496        VibeEngineErrorCode::Unknown => "unknown error",
497        VibeEngineErrorCode::Success => "success",
498        VibeEngineErrorCode::EngineDropped => "engine has been dropped",
499        VibeEngineErrorCode::DatabaseNotOpened => "database is not opened",
500        VibeEngineErrorCode::DatabaseOpenFailed => "database open failed",
501        VibeEngineErrorCode::LogDatabaseOpenFailed => "log database open failed",
502        VibeEngineErrorCode::WorkDatabaseOpenFailed => "work database open failed",
503        VibeEngineErrorCode::DatabaseIOError => "database I/O error",
504        VibeEngineErrorCode::DatabaseTargetNotFound => "database target not found",
505        VibeEngineErrorCode::DatabaseThreadError => "database worker thread error",
506        VibeEngineErrorCode::PostError => "task post failed",
507        VibeEngineErrorCode::Cancelled => "task was cancelled",
508        VibeEngineErrorCode::ParameterEmpty => "required parameter is empty",
509        VibeEngineErrorCode::OAuthError => "OAuth error",
510        VibeEngineErrorCode::ConfigError => "configuration error",
511        VibeEngineErrorCode::IOError => "I/O error",
512        VibeEngineErrorCode::BadRequest => "bad request",
513        VibeEngineErrorCode::RequestError => "request error",
514        VibeEngineErrorCode::InternalServerError => "internal server error",
515        VibeEngineErrorCode::NetworkError => "network error",
516        VibeEngineErrorCode::UnsupportedError => "unsupported operation",
517        VibeEngineErrorCode::TimeoutError => "operation timed out",
518        VibeEngineErrorCode::ConnectError => "connection failed",
519        VibeEngineErrorCode::TlsConnectError => "TLS connection failed",
520        VibeEngineErrorCode::OptionsParseError => "options parse failed",
521        VibeEngineErrorCode::SerdeDeserializeError => "deserialization failed",
522        VibeEngineErrorCode::SerdeSerializeError => "serialization failed",
523        VibeEngineErrorCode::InvalidArgumentLogInfo => "invalid log info argument",
524        VibeEngineErrorCode::MPSCSendError => "channel send failed",
525        VibeEngineErrorCode::GenerateQRError => "QR generation failed",
526        VibeEngineErrorCode::RuntimeError => "runtime error",
527        VibeEngineErrorCode::BeaverError => "internal beaver error",
528        VibeEngineErrorCode::SocketRecvTimeout => "socket receive timed out",
529        VibeEngineErrorCode::SocketClosed => "socket closed",
530        VibeEngineErrorCode::ProtocParseError => "protocol parse failed",
531        VibeEngineErrorCode::SocketNotOpened => "socket is not opened",
532        VibeEngineErrorCode::TaskInterruptionError => "task interrupted",
533        VibeEngineErrorCode::ConnectionClosed => "connection closed",
534        VibeEngineErrorCode::ConnectionExists => "connection already exists",
535        VibeEngineErrorCode::ConnectionClosing => "connection is closing",
536        VibeEngineErrorCode::InternalError => "internal error",
537        VibeEngineErrorCode::NotLoggedInError => "not logged in",
538        VibeEngineErrorCode::PageTokenError => "page token error",
539        VibeEngineErrorCode::ClipboardInitializeError => "clipboard initialization failed",
540        VibeEngineErrorCode::NotSupportedYet => "not supported yet",
541    }
542}
543
544impl serde::Serialize for VibeEngineError {
545    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
546    where
547        S: serde::Serializer,
548    {
549        use serde::ser::SerializeStruct;
550        let mut s = serializer.serialize_struct("VibeEngineError", 5)?;
551        s.serialize_field("code", &self.code())?;
552        s.serialize_field("kind", &self.kind)?;
553        s.serialize_field("message", &self.message)?;
554        s.serialize_field("source", &self.source)?;
555        s.serialize_field("context", &self.context)?;
556        s.end()
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    fn assert_std_error<T: std::error::Error>() {}
565
566    #[test]
567    fn error_model_exposes_kind_message_source_and_context() {
568        assert_std_error::<VibeEngineError>();
569
570        let error = VibeEngineError::from_error_code_msg(
571            VibeEngineErrorCode::TimeoutError,
572            "destroy timed out".to_string(),
573        )
574        .with_source("shutdown queue did not finish")
575        .with_context("engine.destroy_with_timeout");
576
577        assert_eq!(error.code(), VibeEngineErrorCode::TimeoutError.code());
578        assert_eq!(error.kind(), VibeErrorKind::Network);
579        assert_eq!(error.message(), "destroy timed out");
580        assert_eq!(
581            error.source_message(),
582            Some("shutdown queue did not finish")
583        );
584        assert_eq!(
585            error.context(),
586            &["engine.destroy_with_timeout".to_string()]
587        );
588        assert!(error.to_string().contains("destroy timed out"));
589    }
590}
591
592#[cfg(test)]
593mod strict_tests {
594    use super::*;
595    include!(concat!(
596        env!("CARGO_MANIFEST_DIR"),
597        "/test/unit/api/engine_error_tests.rs"
598    ));
599}