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 num_derive::FromPrimitive;
6use std::backtrace::Backtrace;
7use std::fmt;
8
9#[repr(i32)]
10#[derive(Clone, Copy, Debug, FromPrimitive, PartialEq)]
11/// Stable numeric error codes returned by vibe-ready.
12pub enum VibeEngineErrorCode {
13    Unknown = -1,
14
15    Success = 200,
16
17    /// Rust
18    EngineDropped = 100001,
19
20    /// Android, iOS, Web
21    DatabaseNotOpened = 100002,
22
23    /// Android, iOS, Web
24    DatabaseOpenFailed = 100003,
25    LogDatabaseOpenFailed = 100223,
26
27    WorkDatabaseOpenFailed = 132003,
28
29    /// Android, iOS, Web
30    DatabaseIOError = 100004,
31
32    /// Android, iOS, Web
33    DatabaseTargetNotFound = 100005,
34
35    /// Rust
36    DatabaseThreadError = 100006,
37
38    PostError = 100007,
39    /// Task was cancelled before it could complete (B9 scheduler).
40    Cancelled = 100008,
41
42    ParameterEmpty = 100012,
43    OAuthError = 100013,
44    ConfigError = 100014,
45
46    // std::io::Error
47    IOError = 100017,
48    BadRequest = 100018,
49    RequestError = 100019,
50    InternalServerError = 100020,
51    // #[error("network error {0}")]
52    NetworkError = 100021,
53    // #[error("unsupported error")]
54    UnsupportedError = 100022,
55
56    // #[error("timeout error")]
57    TimeoutError = 100023,
58    // #[error("connect error {0}")]
59    ConnectError = 100024,
60    // #[error("connect error {0}")]
61    TlsConnectError = 100025,
62    // #[error("options parse failed {0}")]
63    OptionsParseError = 100026,
64
65    SerdeDeserializeError = 100027,
66    SerdeSerializeError = 100028,
67
68    InvalidArgumentLogInfo = 100029,
69
70    MPSCSendError = 100037,
71
72    GenerateQRError = 10005,
73    RuntimeError = 10006,
74    BeaverError = 10007,
75    SocketRecvTimeout = 10008,
76    SocketClosed = 10009,
77    ProtocParseError = 10010,
78    SocketNotOpened = 10011,
79    TaskInterruptionError = 10012,
80
81    /// Android, iOS, Web
82    ConnectionClosed = 30001,
83
84    /// Android, iOS, Web
85    ConnectionExists = 34001,
86
87    /// Rust
88    ConnectionClosing = 30027,
89
90    /// Android,iOS,Web
91    InternalError = 32002,
92    NotLoggedInError = 32003,
93    PageTokenError = 32004,
94    ClipboardInitializeError = 32005,
95
96    NotSupportedYet = 999999,
97}
98
99impl VibeEngineErrorCode {
100    pub fn code(&self) -> i32 {
101        *self as i32
102    }
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
106/// High-level category for a [`VibeEngineError`].
107pub enum VibeErrorKind {
108    Config,
109    Runtime,
110    Task,
111    Database,
112    Network,
113    Log,
114    Unsupported,
115    Internal,
116    Unknown,
117}
118
119impl VibeErrorKind {
120    fn from_code(code: VibeEngineErrorCode) -> Self {
121        match code {
122            VibeEngineErrorCode::ConfigError
123            | VibeEngineErrorCode::ParameterEmpty
124            | VibeEngineErrorCode::InvalidArgumentLogInfo => Self::Config,
125            VibeEngineErrorCode::RuntimeError
126            | VibeEngineErrorCode::EngineDropped
127            | VibeEngineErrorCode::MPSCSendError => Self::Runtime,
128            VibeEngineErrorCode::PostError
129            | VibeEngineErrorCode::TaskInterruptionError
130            | VibeEngineErrorCode::Cancelled => Self::Task,
131            VibeEngineErrorCode::DatabaseNotOpened
132            | VibeEngineErrorCode::DatabaseOpenFailed
133            | VibeEngineErrorCode::LogDatabaseOpenFailed
134            | VibeEngineErrorCode::WorkDatabaseOpenFailed
135            | VibeEngineErrorCode::DatabaseIOError
136            | VibeEngineErrorCode::DatabaseTargetNotFound
137            | VibeEngineErrorCode::DatabaseThreadError => Self::Database,
138            VibeEngineErrorCode::NetworkError
139            | VibeEngineErrorCode::BadRequest
140            | VibeEngineErrorCode::RequestError
141            | VibeEngineErrorCode::InternalServerError
142            | VibeEngineErrorCode::TimeoutError
143            | VibeEngineErrorCode::ConnectError
144            | VibeEngineErrorCode::TlsConnectError
145            | VibeEngineErrorCode::SocketRecvTimeout
146            | VibeEngineErrorCode::SocketClosed
147            | VibeEngineErrorCode::SocketNotOpened
148            | VibeEngineErrorCode::ConnectionClosed
149            | VibeEngineErrorCode::ConnectionExists
150            | VibeEngineErrorCode::ConnectionClosing => Self::Network,
151            VibeEngineErrorCode::UnsupportedError | VibeEngineErrorCode::NotSupportedYet => {
152                Self::Unsupported
153            }
154            VibeEngineErrorCode::InternalError
155            | VibeEngineErrorCode::IOError
156            | VibeEngineErrorCode::SerdeDeserializeError
157            | VibeEngineErrorCode::SerdeSerializeError
158            | VibeEngineErrorCode::OptionsParseError
159            | VibeEngineErrorCode::GenerateQRError
160            | VibeEngineErrorCode::BeaverError
161            | VibeEngineErrorCode::ProtocParseError
162            | VibeEngineErrorCode::OAuthError
163            | VibeEngineErrorCode::NotLoggedInError
164            | VibeEngineErrorCode::PageTokenError
165            | VibeEngineErrorCode::ClipboardInitializeError => Self::Internal,
166            VibeEngineErrorCode::Success | VibeEngineErrorCode::Unknown => Self::Unknown,
167        }
168    }
169}
170
171#[derive(Debug, Clone)]
172/// Error returned by vibe-ready operations.
173pub struct VibeEngineError {
174    code: i32,
175    kind: VibeErrorKind,
176    message: String,
177    source: Option<String>,
178    context: Vec<String>,
179}
180
181impl fmt::Display for VibeEngineError {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        write!(
184            f,
185            "vibe-ready error [{:?}/{}]: {}",
186            self.kind, self.code, self.message
187        )?;
188        if let Some(source) = &self.source {
189            write!(f, "; source: {source}")?;
190        }
191        if !self.context.is_empty() {
192            write!(f, "; context: {}", self.context.join("; "))?;
193        }
194        Ok(())
195    }
196}
197
198impl std::error::Error for VibeEngineError {}
199
200impl From<DbError> for VibeEngineError {
201    fn from(err: DbError) -> Self {
202        error!("DbError: {:?}, {:?}", err, Backtrace::capture());
203        let db_error = err.clone();
204        let mut error = match err {
205            DbError::OpenFailed => {
206                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseOpenFailed)
207            }
208            DbError::DatabaseIOError => {
209                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseIOError)
210            }
211            DbError::NotOpen => VibeEngineError::from_code(VibeEngineErrorCode::DatabaseOpenFailed),
212            DbError::TargetNotFound => {
213                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseTargetNotFound)
214            }
215            DbError::DatabaseThreadError => {
216                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseThreadError)
217            }
218            DbError::DatabaseUnlockError => {
219                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseOpenFailed)
220            }
221            DbError::NotSupportedYet => {
222                VibeEngineError::from_code(VibeEngineErrorCode::DatabaseIOError)
223            }
224            DbError::JoinError => VibeEngineError::from_code(VibeEngineErrorCode::DatabaseIOError),
225            _ => VibeEngineError::from_code(VibeEngineErrorCode::Unknown),
226        };
227        error.source = Some(format!("{db_error:?}"));
228        error
229    }
230}
231
232impl VibeEngineError {
233    pub fn code(&self) -> i32 {
234        self.code
235    }
236
237    pub fn kind(&self) -> VibeErrorKind {
238        self.kind
239    }
240
241    pub fn message(&self) -> &str {
242        &self.message
243    }
244
245    pub fn source_message(&self) -> Option<&str> {
246        self.source.as_deref()
247    }
248
249    pub fn context(&self) -> &[String] {
250        &self.context
251    }
252
253    pub fn with_source(mut self, source: impl Into<String>) -> Self {
254        self.source = Some(source.into());
255        self
256    }
257
258    pub fn with_context(mut self, context: impl Into<String>) -> Self {
259        self.context.push(context.into());
260        self
261    }
262
263    pub fn from_success() -> Self {
264        Self::from_error_code(VibeEngineErrorCode::Success)
265    }
266
267    pub fn from_u16(code: u16) -> Self {
268        VibeEngineError::from_raw_code(code as i32)
269    }
270
271    pub fn from_u32(code: u32) -> Self {
272        VibeEngineError::from_raw_code(code as i32)
273    }
274
275    pub fn is_success(&self) -> bool {
276        self.code == VibeEngineErrorCode::Success.code()
277    }
278
279    pub fn same_code(&self) -> Self {
280        self.clone()
281    }
282
283    pub fn from_error_code(value: VibeEngineErrorCode) -> Self {
284        VibeEngineError {
285            code: value.code(),
286            kind: VibeErrorKind::from_code(value),
287            message: default_message(value).to_string(),
288            source: None,
289            context: Vec::new(),
290        }
291    }
292
293    pub fn from_code(value: VibeEngineErrorCode) -> Self {
294        Self::from_error_code(value)
295    }
296
297    pub fn from_error_code_msg(value: VibeEngineErrorCode, msg: String) -> Self {
298        VibeEngineError {
299            message: msg,
300            ..Self::from_error_code(value)
301        }
302    }
303
304    pub fn from_task_interruption_error() -> Self {
305        Self::from_error_code(VibeEngineErrorCode::TaskInterruptionError)
306    }
307
308    pub fn from_internal_error() -> Self {
309        Self::from_error_code(VibeEngineErrorCode::InternalError)
310    }
311
312    pub fn from_mpsc_send_error() -> Self {
313        Self::from_error_code(VibeEngineErrorCode::MPSCSendError)
314    }
315
316    pub fn from_parameter_empty() -> Self {
317        Self::from_error_code(VibeEngineErrorCode::ParameterEmpty)
318    }
319    pub fn from_parameter_empty_log(tag: &str) -> Self {
320        let code = VibeEngineErrorCode::ParameterEmpty.code();
321        log_e!(tag, CODE_STR, code);
322        Self::from_error_code(VibeEngineErrorCode::ParameterEmpty)
323    }
324
325    fn from_raw_code(code: i32) -> Self {
326        VibeEngineError {
327            code,
328            kind: VibeErrorKind::Unknown,
329            message: format!("unknown error code {code}"),
330            source: None,
331            context: Vec::new(),
332        }
333    }
334}
335
336fn default_message(code: VibeEngineErrorCode) -> &'static str {
337    match code {
338        VibeEngineErrorCode::Unknown => "unknown error",
339        VibeEngineErrorCode::Success => "success",
340        VibeEngineErrorCode::EngineDropped => "engine has been dropped",
341        VibeEngineErrorCode::DatabaseNotOpened => "database is not opened",
342        VibeEngineErrorCode::DatabaseOpenFailed => "database open failed",
343        VibeEngineErrorCode::LogDatabaseOpenFailed => "log database open failed",
344        VibeEngineErrorCode::WorkDatabaseOpenFailed => "work database open failed",
345        VibeEngineErrorCode::DatabaseIOError => "database I/O error",
346        VibeEngineErrorCode::DatabaseTargetNotFound => "database target not found",
347        VibeEngineErrorCode::DatabaseThreadError => "database worker thread error",
348        VibeEngineErrorCode::PostError => "task post failed",
349        VibeEngineErrorCode::Cancelled => "task was cancelled",
350        VibeEngineErrorCode::ParameterEmpty => "required parameter is empty",
351        VibeEngineErrorCode::OAuthError => "OAuth error",
352        VibeEngineErrorCode::ConfigError => "configuration error",
353        VibeEngineErrorCode::IOError => "I/O error",
354        VibeEngineErrorCode::BadRequest => "bad request",
355        VibeEngineErrorCode::RequestError => "request error",
356        VibeEngineErrorCode::InternalServerError => "internal server error",
357        VibeEngineErrorCode::NetworkError => "network error",
358        VibeEngineErrorCode::UnsupportedError => "unsupported operation",
359        VibeEngineErrorCode::TimeoutError => "operation timed out",
360        VibeEngineErrorCode::ConnectError => "connection failed",
361        VibeEngineErrorCode::TlsConnectError => "TLS connection failed",
362        VibeEngineErrorCode::OptionsParseError => "options parse failed",
363        VibeEngineErrorCode::SerdeDeserializeError => "deserialization failed",
364        VibeEngineErrorCode::SerdeSerializeError => "serialization failed",
365        VibeEngineErrorCode::InvalidArgumentLogInfo => "invalid log info argument",
366        VibeEngineErrorCode::MPSCSendError => "channel send failed",
367        VibeEngineErrorCode::GenerateQRError => "QR generation failed",
368        VibeEngineErrorCode::RuntimeError => "runtime error",
369        VibeEngineErrorCode::BeaverError => "internal beaver error",
370        VibeEngineErrorCode::SocketRecvTimeout => "socket receive timed out",
371        VibeEngineErrorCode::SocketClosed => "socket closed",
372        VibeEngineErrorCode::ProtocParseError => "protocol parse failed",
373        VibeEngineErrorCode::SocketNotOpened => "socket is not opened",
374        VibeEngineErrorCode::TaskInterruptionError => "task interrupted",
375        VibeEngineErrorCode::ConnectionClosed => "connection closed",
376        VibeEngineErrorCode::ConnectionExists => "connection already exists",
377        VibeEngineErrorCode::ConnectionClosing => "connection is closing",
378        VibeEngineErrorCode::InternalError => "internal error",
379        VibeEngineErrorCode::NotLoggedInError => "not logged in",
380        VibeEngineErrorCode::PageTokenError => "page token error",
381        VibeEngineErrorCode::ClipboardInitializeError => "clipboard initialization failed",
382        VibeEngineErrorCode::NotSupportedYet => "not supported yet",
383    }
384}
385
386impl serde::Serialize for VibeEngineError {
387    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
388    where
389        S: serde::Serializer,
390    {
391        use serde::ser::SerializeStruct;
392        let mut s = serializer.serialize_struct("VibeEngineError", 5)?;
393        s.serialize_field("code", &self.code())?;
394        s.serialize_field("kind", &self.kind)?;
395        s.serialize_field("message", &self.message)?;
396        s.serialize_field("source", &self.source)?;
397        s.serialize_field("context", &self.context)?;
398        s.end()
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    fn assert_std_error<T: std::error::Error>() {}
407
408    #[test]
409    fn error_model_exposes_kind_message_source_and_context() {
410        assert_std_error::<VibeEngineError>();
411
412        let error = VibeEngineError::from_error_code_msg(
413            VibeEngineErrorCode::TimeoutError,
414            "destroy timed out".to_string(),
415        )
416        .with_source("shutdown queue did not finish")
417        .with_context("engine.destroy_with_timeout");
418
419        assert_eq!(error.code(), VibeEngineErrorCode::TimeoutError.code());
420        assert_eq!(error.kind(), VibeErrorKind::Network);
421        assert_eq!(error.message(), "destroy timed out");
422        assert_eq!(
423            error.source_message(),
424            Some("shutdown queue did not finish")
425        );
426        assert_eq!(
427            error.context(),
428            &["engine.destroy_with_timeout".to_string()]
429        );
430        assert!(error.to_string().contains("destroy timed out"));
431    }
432}