aimdb_core/
error.rs

1//! Error handling for AimDB core operations
2//!
3//! This module provides a streamlined error type system that works across all AimDB
4//! target platforms: MCU (no_std), edge devices and cloud environments.
5//!
6//! # Platform Compatibility
7//!
8//! The error system is designed with conditional compilation to optimize for
9//! different deployment targets:
10//!
11//! - **MCU/Embedded**: Minimal memory footprint with `no_std` compatibility
12//! - **Edge/Desktop**: Rich error context with standard library features  
13//! - **Cloud**: Full error chains and debugging capabilities with thiserror integration
14//!
15//! # Error Categories
16//!
17//! The [`DbError`] enum covers only the errors actually used in AimDB:
18//!
19//! - **Network**: Connection failures, timeout errors
20//! - **Buffer**: Full, lagged, or closed buffers
21//! - **Outbox**: Not found, full, closed, or duplicate outbox registration
22//! - **Database**: Record not found, invalid operations
23//! - **Configuration**: Missing required configuration
24//! - **Runtime**: Runtime adapter errors
25//! - **Hardware**: MCU hardware errors (embedded only)
26//! - **I/O & JSON**: Standard library integrations (std only)
27
28#[cfg(feature = "std")]
29use thiserror::Error;
30
31#[cfg(feature = "std")]
32use std::io;
33
34/// Streamlined error type for AimDB operations
35///
36/// Only includes errors that are actually used in the codebase,
37/// removing theoretical/unused error variants for simplicity.
38#[derive(Debug)]
39#[cfg_attr(feature = "std", derive(Error))]
40pub enum DbError {
41    // ===== Network Errors (0x1000-0x1FFF) =====
42    /// Connection or timeout failures
43    #[cfg_attr(feature = "std", error("Connection failed to {endpoint}: {reason}"))]
44    ConnectionFailed {
45        #[cfg(feature = "std")]
46        endpoint: String,
47        #[cfg(feature = "std")]
48        reason: String,
49        #[cfg(not(feature = "std"))]
50        _endpoint: (),
51        #[cfg(not(feature = "std"))]
52        _reason: (),
53    },
54
55    // ===== Buffer Errors (0x2000-0x2FFF & 0xA000-0xAFFF) =====
56    /// Buffer is full and cannot accept more items
57    #[cfg_attr(feature = "std", error("Buffer full: {buffer_name} ({size} items)"))]
58    BufferFull {
59        size: u32,
60        #[cfg(feature = "std")]
61        buffer_name: String,
62        #[cfg(not(feature = "std"))]
63        _buffer_name: (),
64    },
65
66    /// Consumer lagged behind producer (SPMC ring buffers)
67    #[cfg_attr(feature = "std", error("Consumer lagged by {lag_count} messages"))]
68    BufferLagged {
69        lag_count: u64,
70        #[cfg(feature = "std")]
71        buffer_name: String,
72        #[cfg(not(feature = "std"))]
73        _buffer_name: (),
74    },
75
76    /// Buffer channel has been closed (shutdown)
77    #[cfg_attr(feature = "std", error("Buffer channel closed: {buffer_name}"))]
78    BufferClosed {
79        #[cfg(feature = "std")]
80        buffer_name: String,
81        #[cfg(not(feature = "std"))]
82        _buffer_name: (),
83    },
84
85    // ===== Database Errors (0x7003-0x7004) =====
86    /// Record type not found in database
87    #[cfg_attr(feature = "std", error("Record type not found: {record_name}"))]
88    RecordNotFound {
89        #[cfg(feature = "std")]
90        record_name: String,
91        #[cfg(not(feature = "std"))]
92        _record_name: (),
93    },
94
95    /// Invalid operation attempted
96    #[cfg_attr(feature = "std", error("Invalid operation '{operation}': {reason}"))]
97    InvalidOperation {
98        #[cfg(feature = "std")]
99        operation: String,
100        #[cfg(feature = "std")]
101        reason: String,
102        #[cfg(not(feature = "std"))]
103        _operation: (),
104        #[cfg(not(feature = "std"))]
105        _reason: (),
106    },
107
108    /// Permission denied for operation
109    #[cfg_attr(feature = "std", error("Permission denied: {operation}"))]
110    PermissionDenied {
111        #[cfg(feature = "std")]
112        operation: String,
113        #[cfg(not(feature = "std"))]
114        _operation: (),
115    },
116
117    // ===== Configuration Errors (0x4000-0x4FFF) =====
118    /// Missing required configuration parameter
119    #[cfg_attr(feature = "std", error("Missing configuration parameter: {parameter}"))]
120    MissingConfiguration {
121        #[cfg(feature = "std")]
122        parameter: String,
123        #[cfg(not(feature = "std"))]
124        _parameter: (),
125    },
126
127    // ===== Runtime Errors (0x7002 & 0x5000-0x5FFF) =====
128    /// Runtime execution error (task spawning, scheduling, etc.)
129    #[cfg_attr(feature = "std", error("Runtime error: {message}"))]
130    RuntimeError {
131        #[cfg(feature = "std")]
132        message: String,
133        #[cfg(not(feature = "std"))]
134        _message: (),
135    },
136
137    /// Resource temporarily unavailable (used by adapters)
138    #[cfg_attr(feature = "std", error("Resource unavailable: {resource_name}"))]
139    ResourceUnavailable {
140        resource_type: u8,
141        #[cfg(feature = "std")]
142        resource_name: String,
143        #[cfg(not(feature = "std"))]
144        _resource_name: (),
145    },
146
147    // ===== Hardware Errors (0x6000-0x6FFF) - Embedded Only =====
148    /// Hardware-specific errors for embedded/MCU environments
149    #[cfg_attr(
150        feature = "std",
151        error("Hardware error: component {component}, code 0x{error_code:04X}")
152    )]
153    HardwareError {
154        component: u8,
155        error_code: u16,
156        #[cfg(feature = "std")]
157        description: String,
158        #[cfg(not(feature = "std"))]
159        _description: (),
160    },
161
162    // ===== Internal Errors (0x7001) =====
163    /// Internal error for unexpected conditions
164    #[cfg_attr(feature = "std", error("Internal error (0x{code:04X}): {message}"))]
165    Internal {
166        code: u32,
167        #[cfg(feature = "std")]
168        message: String,
169        #[cfg(not(feature = "std"))]
170        _message: (),
171    },
172
173    // ===== Sync API Errors (0xB000-0xBFFF) - std only =====
174    /// Failed to attach database to runtime thread
175    #[cfg(feature = "std")]
176    #[error("Failed to attach database: {message}")]
177    AttachFailed { message: String },
178
179    /// Failed to detach database from runtime thread
180    #[cfg(feature = "std")]
181    #[error("Failed to detach database: {message}")]
182    DetachFailed { message: String },
183
184    /// Timeout while setting a value
185    #[cfg(feature = "std")]
186    #[error("Timeout while setting value")]
187    SetTimeout,
188
189    /// Timeout while getting a value
190    #[cfg(feature = "std")]
191    #[error("Timeout while getting value")]
192    GetTimeout,
193
194    /// Runtime thread has shut down
195    #[cfg(feature = "std")]
196    #[error("Runtime thread has shut down")]
197    RuntimeShutdown,
198
199    // ===== Standard Library Integrations (std only) =====
200    /// I/O operation error
201    #[cfg(feature = "std")]
202    #[error("I/O error: {source}")]
203    Io {
204        #[from]
205        source: io::Error,
206    },
207
208    /// I/O operation error with context
209    #[cfg(feature = "std")]
210    #[error("I/O error: {context}: {source}")]
211    IoWithContext {
212        context: String,
213        #[source]
214        source: io::Error,
215    },
216
217    /// JSON serialization error
218    #[cfg(feature = "std")]
219    #[error("JSON error: {source}")]
220    Json {
221        #[from]
222        source: serde_json::Error,
223    },
224
225    /// JSON serialization error with context
226    #[cfg(feature = "std")]
227    #[error("JSON error: {context}: {source}")]
228    JsonWithContext {
229        context: String,
230        #[source]
231        source: serde_json::Error,
232    },
233}
234
235// ===== no_std Display Implementation =====
236#[cfg(not(feature = "std"))]
237impl core::fmt::Display for DbError {
238    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
239        let (code, message) = match self {
240            DbError::ConnectionFailed { .. } => (0x1002, "Connection failed"),
241            DbError::BufferFull { .. } => (0x2002, "Buffer full"),
242            DbError::BufferLagged { .. } => (0xA001, "Buffer consumer lagged"),
243            DbError::BufferClosed { .. } => (0xA002, "Buffer channel closed"),
244            DbError::RecordNotFound { .. } => (0x7003, "Record not found"),
245            DbError::InvalidOperation { .. } => (0x7004, "Invalid operation"),
246            DbError::PermissionDenied { .. } => (0x7005, "Permission denied"),
247            DbError::MissingConfiguration { .. } => (0x4002, "Missing configuration"),
248            DbError::RuntimeError { .. } => (0x7002, "Runtime error"),
249            DbError::ResourceUnavailable { .. } => (0x5002, "Resource unavailable"),
250            DbError::HardwareError { .. } => (0x6001, "Hardware error"),
251            DbError::Internal { .. } => (0x7001, "Internal error"),
252        };
253        write!(f, "Error 0x{:04X}: {}", code, message)
254    }
255}
256
257// ===== DbError Implementation =====
258impl DbError {
259    // Resource type constants
260    pub const RESOURCE_TYPE_MEMORY: u8 = 0;
261    pub const RESOURCE_TYPE_FILE_HANDLE: u8 = 1;
262    pub const RESOURCE_TYPE_SOCKET: u8 = 2;
263    pub const RESOURCE_TYPE_BUFFER: u8 = 3;
264    pub const RESOURCE_TYPE_THREAD: u8 = 4;
265    pub const RESOURCE_TYPE_MUTEX: u8 = 5;
266    pub const RESOURCE_TYPE_SEMAPHORE: u8 = 6;
267    pub const RESOURCE_TYPE_CHANNEL: u8 = 7;
268    pub const RESOURCE_TYPE_WOULD_BLOCK: u8 = 255;
269
270    /// Creates a hardware error for embedded environments
271    pub fn hardware_error(component: u8, error_code: u16) -> Self {
272        DbError::HardwareError {
273            component,
274            error_code,
275            #[cfg(feature = "std")]
276            description: String::new(),
277            #[cfg(not(feature = "std"))]
278            _description: (),
279        }
280    }
281
282    /// Creates an internal error with a specific error code
283    pub fn internal(code: u32) -> Self {
284        DbError::Internal {
285            code,
286            #[cfg(feature = "std")]
287            message: String::new(),
288            #[cfg(not(feature = "std"))]
289            _message: (),
290        }
291    }
292
293    /// Returns true if this is a network-related error
294    pub fn is_network_error(&self) -> bool {
295        matches!(self, DbError::ConnectionFailed { .. })
296    }
297
298    /// Returns true if this is a capacity-related error  
299    pub fn is_capacity_error(&self) -> bool {
300        matches!(self, DbError::BufferFull { .. })
301    }
302
303    /// Returns true if this is a hardware-related error
304    pub fn is_hardware_error(&self) -> bool {
305        matches!(self, DbError::HardwareError { .. })
306    }
307
308    /// Returns a numeric error code for embedded environments
309    pub const fn error_code(&self) -> u32 {
310        match self {
311            // Network errors: 0x1000-0x1FFF
312            DbError::ConnectionFailed { .. } => 0x1002,
313
314            // Capacity errors: 0x2000-0x2FFF
315            DbError::BufferFull { .. } => 0x2002,
316
317            // Configuration errors: 0x4000-0x4FFF
318            DbError::MissingConfiguration { .. } => 0x4002,
319
320            // Resource errors: 0x5000-0x5FFF
321            DbError::ResourceUnavailable { .. } => 0x5002,
322
323            // Hardware errors: 0x6000-0x6FFF
324            DbError::HardwareError { .. } => 0x6001,
325
326            // Internal errors: 0x7000-0x7FFF
327            DbError::Internal { .. } => 0x7001,
328            DbError::RuntimeError { .. } => 0x7002,
329            DbError::RecordNotFound { .. } => 0x7003,
330            DbError::InvalidOperation { .. } => 0x7004,
331            DbError::PermissionDenied { .. } => 0x7005,
332
333            // I/O errors: 0x8000-0x8FFF (std only)
334            #[cfg(feature = "std")]
335            DbError::Io { .. } => 0x8001,
336            #[cfg(feature = "std")]
337            DbError::IoWithContext { .. } => 0x8002,
338
339            // JSON errors: 0x9000-0x9FFF (std only)
340            #[cfg(feature = "std")]
341            DbError::Json { .. } => 0x9001,
342            #[cfg(feature = "std")]
343            DbError::JsonWithContext { .. } => 0x9002,
344
345            // Buffer operation errors: 0xA000-0xAFFF
346            DbError::BufferLagged { .. } => 0xA001,
347            DbError::BufferClosed { .. } => 0xA002,
348
349            // Sync API errors: 0xB000-0xBFFF (std only)
350            #[cfg(feature = "std")]
351            DbError::AttachFailed { .. } => 0xB001,
352            #[cfg(feature = "std")]
353            DbError::DetachFailed { .. } => 0xB002,
354            #[cfg(feature = "std")]
355            DbError::SetTimeout => 0xB003,
356            #[cfg(feature = "std")]
357            DbError::GetTimeout => 0xB004,
358            #[cfg(feature = "std")]
359            DbError::RuntimeShutdown => 0xB005,
360        }
361    }
362
363    /// Returns the error category (high nibble)
364    pub const fn error_category(&self) -> u32 {
365        self.error_code() & 0xF000
366    }
367
368    /// Helper to prepend context to a message string
369    #[cfg(feature = "std")]
370    fn prepend_context<S: Into<String>>(existing: &mut String, new_context: S) {
371        let new_context = new_context.into();
372        existing.insert_str(0, ": ");
373        existing.insert_str(0, &new_context);
374    }
375
376    /// Adds additional context to an error (std only)
377    #[cfg(feature = "std")]
378    pub fn with_context<S: Into<String>>(self, context: S) -> Self {
379        match self {
380            DbError::ConnectionFailed {
381                mut reason,
382                endpoint,
383            } => {
384                Self::prepend_context(&mut reason, context);
385                DbError::ConnectionFailed { endpoint, reason }
386            }
387            DbError::BufferFull {
388                size,
389                mut buffer_name,
390            } => {
391                Self::prepend_context(&mut buffer_name, context);
392                DbError::BufferFull { size, buffer_name }
393            }
394            DbError::BufferLagged {
395                lag_count,
396                mut buffer_name,
397            } => {
398                Self::prepend_context(&mut buffer_name, context);
399                DbError::BufferLagged {
400                    lag_count,
401                    buffer_name,
402                }
403            }
404            DbError::BufferClosed { mut buffer_name } => {
405                Self::prepend_context(&mut buffer_name, context);
406                DbError::BufferClosed { buffer_name }
407            }
408            DbError::RecordNotFound { mut record_name } => {
409                Self::prepend_context(&mut record_name, context);
410                DbError::RecordNotFound { record_name }
411            }
412            DbError::InvalidOperation {
413                operation,
414                mut reason,
415            } => {
416                Self::prepend_context(&mut reason, context);
417                DbError::InvalidOperation { operation, reason }
418            }
419            DbError::PermissionDenied { mut operation } => {
420                Self::prepend_context(&mut operation, context);
421                DbError::PermissionDenied { operation }
422            }
423            DbError::MissingConfiguration { mut parameter } => {
424                Self::prepend_context(&mut parameter, context);
425                DbError::MissingConfiguration { parameter }
426            }
427            DbError::RuntimeError { mut message } => {
428                Self::prepend_context(&mut message, context);
429                DbError::RuntimeError { message }
430            }
431            DbError::ResourceUnavailable {
432                resource_type,
433                mut resource_name,
434            } => {
435                Self::prepend_context(&mut resource_name, context);
436                DbError::ResourceUnavailable {
437                    resource_type,
438                    resource_name,
439                }
440            }
441            DbError::HardwareError {
442                component,
443                error_code,
444                mut description,
445            } => {
446                Self::prepend_context(&mut description, context);
447                DbError::HardwareError {
448                    component,
449                    error_code,
450                    description,
451                }
452            }
453            DbError::Internal { code, mut message } => {
454                Self::prepend_context(&mut message, context);
455                DbError::Internal { code, message }
456            }
457            // Sync API errors that support context (std only)
458            #[cfg(feature = "std")]
459            DbError::AttachFailed { mut message } => {
460                Self::prepend_context(&mut message, context);
461                DbError::AttachFailed { message }
462            }
463            #[cfg(feature = "std")]
464            DbError::DetachFailed { mut message } => {
465                Self::prepend_context(&mut message, context);
466                DbError::DetachFailed { message }
467            }
468            // Sync timeout and shutdown errors don't have context fields (std only)
469            #[cfg(feature = "std")]
470            DbError::SetTimeout => DbError::SetTimeout,
471            #[cfg(feature = "std")]
472            DbError::GetTimeout => DbError::GetTimeout,
473            #[cfg(feature = "std")]
474            DbError::RuntimeShutdown => DbError::RuntimeShutdown,
475            // Convert simple I/O and JSON errors to context variants (std only)
476            #[cfg(feature = "std")]
477            DbError::Io { source } => DbError::IoWithContext {
478                context: context.into(),
479                source,
480            },
481            #[cfg(feature = "std")]
482            DbError::Json { source } => DbError::JsonWithContext {
483                context: context.into(),
484                source,
485            },
486            // Prepend to existing context for context variants (std only)
487            #[cfg(feature = "std")]
488            DbError::IoWithContext {
489                context: mut ctx,
490                source,
491            } => {
492                Self::prepend_context(&mut ctx, context);
493                DbError::IoWithContext {
494                    context: ctx,
495                    source,
496                }
497            }
498            #[cfg(feature = "std")]
499            DbError::JsonWithContext {
500                context: mut ctx,
501                source,
502            } => {
503                Self::prepend_context(&mut ctx, context);
504                DbError::JsonWithContext {
505                    context: ctx,
506                    source,
507                }
508            }
509        }
510    }
511
512    /// Converts this error into an anyhow::Error (std only)
513    #[cfg(feature = "std")]
514    pub fn into_anyhow(self) -> anyhow::Error {
515        self.into()
516    }
517}
518
519/// Type alias for Results using DbError
520pub type DbResult<T> = Result<T, DbError>;
521
522// ============================================================================
523// Error Conversions
524// ============================================================================
525
526/// Convert executor errors to database errors
527///
528/// This allows runtime adapters to return `ExecutorError` while the core
529/// database works with `DbError` for consistency across the API.
530impl From<aimdb_executor::ExecutorError> for DbError {
531    fn from(err: aimdb_executor::ExecutorError) -> Self {
532        use aimdb_executor::ExecutorError;
533
534        match err {
535            ExecutorError::SpawnFailed { message } => {
536                #[cfg(feature = "std")]
537                {
538                    DbError::RuntimeError { message }
539                }
540                #[cfg(not(feature = "std"))]
541                {
542                    let _ = message; // Avoid unused warnings
543                    DbError::RuntimeError { _message: () }
544                }
545            }
546            ExecutorError::RuntimeUnavailable { message } => {
547                #[cfg(feature = "std")]
548                {
549                    DbError::RuntimeError { message }
550                }
551                #[cfg(not(feature = "std"))]
552                {
553                    let _ = message; // Avoid unused warnings
554                    DbError::RuntimeError { _message: () }
555                }
556            }
557            ExecutorError::TaskJoinFailed { message } => {
558                #[cfg(feature = "std")]
559                {
560                    DbError::RuntimeError { message }
561                }
562                #[cfg(not(feature = "std"))]
563                {
564                    let _ = message; // Avoid unused warnings
565                    DbError::RuntimeError { _message: () }
566                }
567            }
568        }
569    }
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575
576    #[test]
577    fn test_error_size_constraint() {
578        let size = core::mem::size_of::<DbError>();
579        assert!(
580            size <= 64,
581            "DbError size ({} bytes) exceeds 64-byte embedded limit",
582            size
583        );
584    }
585
586    #[test]
587    fn test_error_codes() {
588        let connection_error = DbError::ConnectionFailed {
589            #[cfg(feature = "std")]
590            endpoint: "localhost".to_string(),
591            #[cfg(feature = "std")]
592            reason: "timeout".to_string(),
593            #[cfg(not(feature = "std"))]
594            _endpoint: (),
595            #[cfg(not(feature = "std"))]
596            _reason: (),
597        };
598        assert_eq!(connection_error.error_code(), 0x1002);
599        assert_eq!(connection_error.error_category(), 0x1000);
600
601        let buffer_error = DbError::BufferFull {
602            size: 1024,
603            #[cfg(feature = "std")]
604            buffer_name: String::new(),
605            #[cfg(not(feature = "std"))]
606            _buffer_name: (),
607        };
608        assert_eq!(buffer_error.error_code(), 0x2002);
609    }
610
611    #[test]
612    fn test_helper_methods() {
613        let connection_error = DbError::ConnectionFailed {
614            #[cfg(feature = "std")]
615            endpoint: "localhost".to_string(),
616            #[cfg(feature = "std")]
617            reason: "timeout".to_string(),
618            #[cfg(not(feature = "std"))]
619            _endpoint: (),
620            #[cfg(not(feature = "std"))]
621            _reason: (),
622        };
623
624        assert!(connection_error.is_network_error());
625        assert!(!connection_error.is_capacity_error());
626        assert!(!connection_error.is_hardware_error());
627
628        let hardware_error = DbError::hardware_error(2, 404);
629        assert!(hardware_error.is_hardware_error());
630
631        let internal_error = DbError::internal(500);
632        assert!(matches!(
633            internal_error,
634            DbError::Internal { code: 500, .. }
635        ));
636    }
637
638    #[cfg(feature = "std")]
639    #[test]
640    fn test_error_context() {
641        let error = DbError::ConnectionFailed {
642            endpoint: "localhost:5432".to_string(),
643            reason: "timeout".to_string(),
644        }
645        .with_context("Database connection")
646        .with_context("Application startup");
647
648        if let DbError::ConnectionFailed { reason, .. } = error {
649            assert_eq!(reason, "Application startup: Database connection: timeout");
650        } else {
651            panic!("Expected ConnectionFailed");
652        }
653    }
654
655    #[cfg(feature = "std")]
656    #[test]
657    fn test_io_json_conversions() {
658        let io_error = std::io::Error::other("File not found");
659        let db_error: DbError = io_error.into();
660        assert!(matches!(db_error, DbError::Io { .. }));
661
662        let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
663        let db_error: DbError = json_error.into();
664        assert!(matches!(db_error, DbError::Json { .. }));
665    }
666}