diff --git a/Cargo.lock b/Cargo.lock index 237e946b..3a982bf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,7 +18,9 @@ version = "0.1.0" name = "aimdb-core" version = "0.1.0" dependencies = [ + "anyhow", "heapless", + "serde_json", "thiserror", ] @@ -26,6 +28,12 @@ dependencies = [ name = "aimdb-examples" version = "0.1.0" +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "byteorder" version = "1.5.0" @@ -51,6 +59,18 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + [[package]] name = "proc-macro2" version = "1.0.101" @@ -69,6 +89,54 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 1252c8b5..aa989784 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,10 @@ serde = { version = "1.0", features = ["derive"] } # Error handling thiserror = "2.0.16" +anyhow = "1.0" + +# Serialization - extend with error handling +serde_json = "1.0" # Basic observability tracing = "0.1" diff --git a/aimdb-core/Cargo.toml b/aimdb-core/Cargo.toml index 30af3eb8..c88eb1de 100644 --- a/aimdb-core/Cargo.toml +++ b/aimdb-core/Cargo.toml @@ -7,11 +7,13 @@ description = "Core database engine for AimDB - async in-memory storage with rea [features] default = ["std"] -std = ["thiserror"] +std = ["thiserror", "anyhow", "serde_json"] [dependencies] # Error handling - only for std environments thiserror = { workspace = true, optional = true } +anyhow = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } [dev-dependencies] # For no_std testing diff --git a/aimdb-core/src/error.rs b/aimdb-core/src/error.rs index 71313d23..7311c549 100644 --- a/aimdb-core/src/error.rs +++ b/aimdb-core/src/error.rs @@ -35,6 +35,8 @@ //! - **Resource** (0x5000-0x5FFF): Allocation failures, unavailable resources, system limits //! - **Hardware** (0x6000-0x6FFF): MCU peripheral errors, device initialization, hardware faults //! - **Internal** (0x7000-0x7FFF): Unexpected conditions and system errors +//! - **I/O Operations** (0x8000-0x8FFF): File system and I/O errors, including context variants +//! - **JSON Processing** (0x9000-0x9FFF): JSON serialization/deserialization errors, including context variants //! //! ## Platform-Specific Display Behavior //! @@ -47,8 +49,111 @@ //! - **Display formatting**: <1μs on embedded, <10μs on std platforms //! - **Memory usage**: Zero heap allocation on embedded targets //! +//! # Enhanced Error Features (std only) +//! +//! When the `std` feature is enabled (default), AimDB provides rich error handling capabilities: +//! +//! - **Error Chaining**: Add context to errors with the `with_context()` method (works on all error types) +//! - **Standard Library Integration**: Automatic conversions from `std::io::Error` and `serde_json::Error` +//! - **Anyhow Compatibility**: Seamless integration with `anyhow` for application boundaries +//! - **Rich Error Messages**: Full context and error source chains +//! - **Consistent Context API**: All error variants support context chaining, including I/O and JSON errors +//! +//! ## Error Chaining Example +//! +//! ```rust +//! # #[cfg(feature = "std")] +//! # { +//! use aimdb_core::DbError; +//! +//! let error = DbError::network_timeout(5000) +//! .with_context("Failed to connect to database server") +//! .with_context("During application startup"); +//! +//! println!("{}", error); +//! // "Network timeout after 5000ms: During application startup: Failed to connect to database server" +//! # } +//! ``` +//! +//! ## Standard Library Integration +//! +//! ```rust +//! # #[cfg(feature = "std")] +//! # { +//! use aimdb_core::DbError; +//! use std::fs::File; +//! +//! // Automatic conversion from std::io::Error +//! let result: Result = File::open("nonexistent.txt") +//! .map_err(DbError::from); +//! +//! // Adding context to I/O errors now works consistently +//! let contextualized_result: Result = File::open("config.txt") +//! .map_err(DbError::from) +//! .map_err(|e| e.with_context("Loading application configuration")); +//! +//! // Or using the ? operator in functions +//! fn read_config() -> Result { +//! let content = std::fs::read_to_string("config.txt")?; +//! Ok(content) +//! } +//! # } +//! ``` +//! +//! ## Anyhow Integration +//! +//! ```rust +//! # #[cfg(feature = "std")] +//! # { +//! use aimdb_core::DbError; +//! use anyhow::Context; +//! +//! fn application_main() -> anyhow::Result<()> { +//! // Consistent context handling works across all error types +//! let io_error = std::io::Error::other("File not found"); +//! let db_error = DbError::from(io_error) +//! .with_context("Loading configuration file") +//! .with_context("Application initialization"); +//! +//! Err(db_error.into_anyhow()) +//! .context("Database initialization failed") +//! .context("Application startup failed") +//! } +//! # } +//! ``` +//! //! # Usage Examples //! +//! ## Consistent Context Handling +//! +//! The `with_context()` method now works consistently across all error types: +//! +//! ```rust +//! # #[cfg(feature = "std")] +//! # { +//! use aimdb_core::DbError; +//! +//! // I/O errors can now receive context +//! let io_error = std::io::Error::other("Permission denied"); +//! let contextualized = DbError::from(io_error) +//! .with_context("Reading configuration file") +//! .with_context("Application startup"); +//! +//! // JSON errors can now receive context +//! let json_str = r#"{"invalid": json}"#; +//! let json_result: Result = +//! serde_json::from_str(json_str) +//! .map_err(DbError::from) +//! .map_err(|e| e.with_context("Parsing user input")) +//! .map_err(|e| e.with_context("Configuration validation")); +//! +//! // Context chaining works on all error types +//! let network_error = DbError::network_timeout(5000) +//! .with_context("Connecting to database") +//! .with_context("Service initialization"); +//! # } +//! ``` +//! //! ## Basic Error Handling //! //! ```rust @@ -161,6 +266,10 @@ #[cfg(feature = "std")] use thiserror::Error; +// Additional std-only imports for enhanced error handling +#[cfg(feature = "std")] +use std::io; + /// Unified error type for all AimDB operations across platforms /// /// This enum covers all error scenarios that can occur during AimDB operations, @@ -323,6 +432,40 @@ pub enum DbError { #[cfg(not(feature = "std"))] _message: (), }, + + /// I/O operation errors (std only) + #[cfg(feature = "std")] + #[error("I/O error: {source}")] + Io { + #[from] + source: io::Error, + }, + + /// I/O operation errors with additional context (std only) + #[cfg(feature = "std")] + #[error("I/O error: {context}: {source}")] + IoWithContext { + context: String, + #[source] + source: io::Error, + }, + + /// JSON serialization errors (std only) + #[cfg(feature = "std")] + #[error("JSON error: {source}")] + Json { + #[from] + source: serde_json::Error, + }, + + /// JSON serialization errors with additional context (std only) + #[cfg(feature = "std")] + #[error("JSON error: {context}: {source}")] + JsonWithContext { + context: String, + #[source] + source: serde_json::Error, + }, } #[cfg(not(feature = "std"))] @@ -346,6 +489,16 @@ impl core::fmt::Display for DbError { DbError::HardwareError { .. } => (0x6001, "Hardware error"), DbError::PeripheralInitFailed { .. } => (0x6002, "Peripheral initialization failed"), DbError::Internal { .. } => (0x7001, "Internal error"), + + // Standard library only errors (conditionally compiled) + #[cfg(feature = "std")] + DbError::Io { .. } => (0x8001, "I/O error"), + #[cfg(feature = "std")] + DbError::IoWithContext { .. } => (0x8002, "I/O error with context"), + #[cfg(feature = "std")] + DbError::Json { .. } => (0x9001, "JSON error"), + #[cfg(feature = "std")] + DbError::JsonWithContext { .. } => (0x9002, "JSON error with context"), }; write!(f, "Error 0x{:04X}: {}", code, message) } @@ -506,6 +659,19 @@ impl DbError { // Internal errors: 0x7000-0x7FFF DbError::Internal { .. } => 0x7001, + + // Standard library only errors (conditionally compiled) + // I/O errors: 0x8000-0x8FFF + #[cfg(feature = "std")] + DbError::Io { .. } => 0x8001, + #[cfg(feature = "std")] + DbError::IoWithContext { .. } => 0x8002, + + // JSON errors: 0x9000-0x9FFF + #[cfg(feature = "std")] + DbError::Json { .. } => 0x9001, + #[cfg(feature = "std")] + DbError::JsonWithContext { .. } => 0x9002, } } @@ -526,8 +692,248 @@ impl DbError { pub const fn error_category(&self) -> u32 { self.error_code() & 0xF000 } + + /// Helper function to prepend context to an existing message string + /// + /// This function encapsulates the logic for combining new context with existing + /// error messages, handling the case where the existing message might be empty. + /// Uses in-place string modification to avoid unnecessary allocations. + #[cfg(feature = "std")] + fn prepend_context>(existing: &mut String, new_context: S) { + let new_context = new_context.into(); + if existing.is_empty() { + *existing = new_context; + } else { + existing.insert_str(0, ": "); + existing.insert_str(0, &new_context); + } + } + + /// Helper function to prepend context to a message string (always prepends) + /// + /// This function always prepends the new context, used for fields that + /// always contain some default content. Uses in-place string modification + /// to avoid unnecessary allocations in the error handling hot path. + #[cfg(feature = "std")] + fn prepend_context_always>(existing: &mut String, new_context: S) { + let new_context = new_context.into(); + existing.insert_str(0, ": "); + existing.insert_str(0, &new_context); + } + + /// Adds additional context to an error (std only) + /// + /// This method allows chaining additional context information to errors, + /// useful for providing more detailed error information in edge and cloud + /// environments where memory is not as constrained. + /// + /// # Examples + /// + /// ```rust + /// # #[cfg(feature = "std")] + /// # { + /// use aimdb_core::DbError; + /// + /// let error = DbError::network_timeout(5000) + /// .with_context("Failed to connect to database server") + /// .with_context("During application startup"); + /// # } + /// ``` + #[cfg(feature = "std")] + pub fn with_context>(self, context: S) -> Self { + match self { + DbError::NetworkTimeout { + context: mut ctx, + timeout_ms, + } => { + Self::prepend_context(&mut ctx, context); + DbError::NetworkTimeout { + timeout_ms, + context: ctx, + } + } + DbError::ConnectionFailed { + mut reason, + endpoint, + } => { + Self::prepend_context_always(&mut reason, context); + DbError::ConnectionFailed { endpoint, reason } + } + DbError::ProtocolError { + error_code, + mut message, + } => { + Self::prepend_context_always(&mut message, context); + DbError::ProtocolError { + error_code, + message, + } + } + DbError::CapacityExceeded { + current, + limit, + mut resource_type, + } => { + Self::prepend_context_always(&mut resource_type, context); + DbError::CapacityExceeded { + current, + limit, + resource_type, + } + } + DbError::BufferFull { + size, + mut buffer_name, + } => { + Self::prepend_context_always(&mut buffer_name, context); + DbError::BufferFull { size, buffer_name } + } + DbError::SerializationFailed { + format, + mut details, + } => { + Self::prepend_context_always(&mut details, context); + DbError::SerializationFailed { format, details } + } + DbError::InvalidDataFormat { + expected_format, + received_format, + mut description, + } => { + Self::prepend_context_always(&mut description, context); + DbError::InvalidDataFormat { + expected_format, + received_format, + description, + } + } + DbError::InvalidConfiguration { + mut parameter, + value, + } => { + Self::prepend_context_always(&mut parameter, context); + DbError::InvalidConfiguration { parameter, value } + } + DbError::MissingConfiguration { mut parameter } => { + Self::prepend_context_always(&mut parameter, context); + DbError::MissingConfiguration { parameter } + } + DbError::ResourceAllocationFailed { + resource_type, + requested_size, + mut details, + } => { + Self::prepend_context_always(&mut details, context); + DbError::ResourceAllocationFailed { + resource_type, + requested_size, + details, + } + } + DbError::ResourceUnavailable { + resource_type, + mut resource_name, + } => { + Self::prepend_context_always(&mut resource_name, context); + DbError::ResourceUnavailable { + resource_type, + resource_name, + } + } + DbError::HardwareError { + component, + error_code, + mut description, + } => { + Self::prepend_context_always(&mut description, context); + DbError::HardwareError { + component, + error_code, + description, + } + } + DbError::PeripheralInitFailed { + peripheral_id, + mut peripheral, + } => { + Self::prepend_context_always(&mut peripheral, context); + DbError::PeripheralInitFailed { + peripheral_id, + peripheral, + } + } + DbError::Internal { code, mut message } => { + Self::prepend_context_always(&mut message, context); + DbError::Internal { code, message } + } + + // For Io and Json errors, convert to context variants + #[cfg(feature = "std")] + DbError::Io { source } => DbError::IoWithContext { + context: context.into(), + source, + }, + #[cfg(feature = "std")] + DbError::Json { source } => DbError::JsonWithContext { + context: context.into(), + source, + }, + + // For context variants, prepend to existing context + #[cfg(feature = "std")] + DbError::IoWithContext { + context: mut ctx, + source, + } => { + Self::prepend_context_always(&mut ctx, context); + DbError::IoWithContext { + context: ctx, + source, + } + } + #[cfg(feature = "std")] + DbError::JsonWithContext { + context: mut ctx, + source, + } => { + Self::prepend_context_always(&mut ctx, context); + DbError::JsonWithContext { + context: ctx, + source, + } + } + } + } + + /// Converts this error into an anyhow::Error (std only) + /// + /// This method provides seamless integration with anyhow for application + /// boundaries, allowing AimDB errors to be used in anyhow error chains. + /// + /// # Examples + /// + /// ```rust + /// # #[cfg(feature = "std")] + /// # { + /// use aimdb_core::DbError; + /// use anyhow::Context; + /// + /// fn application_boundary() -> anyhow::Result<()> { + /// let db_error = DbError::network_timeout(5000); + /// Err(db_error.into_anyhow()) + /// .context("Failed to initialize database connection") + /// } + /// # } + /// ``` + #[cfg(feature = "std")] + pub fn into_anyhow(self) -> anyhow::Error { + self.into() + } } +// Note: anyhow::Error automatically implements From via its blanket +// implementation for any type that implements std::error::Error + Send + Sync + 'static + /// Type alias for Results using DbError /// /// This is the standard Result type used throughout AimDB for operations @@ -830,7 +1236,7 @@ mod tests { use std::collections::HashSet; let mut codes = HashSet::new(); - let errors = [ + let mut errors = vec![ DbError::NetworkTimeout { timeout_ms: 0, #[cfg(feature = "std")] @@ -868,8 +1274,93 @@ mod tests { #[cfg(not(feature = "std"))] _buffer_name: (), }, + DbError::SerializationFailed { + format: 0, + #[cfg(feature = "std")] + details: String::new(), + #[cfg(not(feature = "std"))] + _details: (), + }, + DbError::InvalidDataFormat { + expected_format: 0, + received_format: 0, + #[cfg(feature = "std")] + description: String::new(), + #[cfg(not(feature = "std"))] + _description: (), + }, + DbError::InvalidConfiguration { + #[cfg(feature = "std")] + parameter: String::new(), + #[cfg(feature = "std")] + value: String::new(), + #[cfg(not(feature = "std"))] + _parameter: (), + }, + DbError::MissingConfiguration { + #[cfg(feature = "std")] + parameter: String::new(), + #[cfg(not(feature = "std"))] + _parameter: (), + }, + DbError::ResourceAllocationFailed { + resource_type: 0, + requested_size: 0, + #[cfg(feature = "std")] + details: String::new(), + #[cfg(not(feature = "std"))] + _details: (), + }, + DbError::ResourceUnavailable { + resource_type: 0, + #[cfg(feature = "std")] + resource_name: String::new(), + #[cfg(not(feature = "std"))] + _resource_name: (), + }, + DbError::HardwareError { + component: 0, + error_code: 0, + #[cfg(feature = "std")] + description: String::new(), + #[cfg(not(feature = "std"))] + _description: (), + }, + DbError::PeripheralInitFailed { + peripheral_id: 0, + #[cfg(feature = "std")] + peripheral: String::new(), + #[cfg(not(feature = "std"))] + _peripheral: (), + }, + DbError::Internal { + code: 0, + #[cfg(feature = "std")] + message: String::new(), + #[cfg(not(feature = "std"))] + _message: (), + }, ]; + // Add std-only errors if the std feature is enabled + #[cfg(feature = "std")] + { + errors.push(DbError::Io { + source: std::io::Error::other("test"), + }); + errors.push(DbError::IoWithContext { + context: "test context".to_string(), + source: std::io::Error::other("test"), + }); + errors.push(DbError::Json { + source: serde_json::from_str::("invalid").unwrap_err(), + }); + errors.push(DbError::JsonWithContext { + context: "test context".to_string(), + source: serde_json::from_str::("invalid").unwrap_err(), + }); + } + for error in &errors { let code = error.error_code(); assert!(codes.insert(code), "Duplicate error code: 0x{:04X}", code); @@ -959,6 +1450,378 @@ mod tests { assert!(display_msg.contains("memory buffer")); } + #[cfg(feature = "std")] + #[test] + fn test_error_chaining_with_context() { + // Test that with_context() properly chains error context + let base_error = DbError::network_timeout(5000); + let chained_error = base_error + .with_context("Failed to connect to database server") + .with_context("During application startup"); + + if let DbError::NetworkTimeout { context, .. } = chained_error { + assert_eq!( + context, + "During application startup: Failed to connect to database server" + ); + } else { + panic!("Expected NetworkTimeout error"); + } + + // Test chaining on different error types + let capacity_error = DbError::capacity_exceeded_with_type(2048, 1024, "memory") + .with_context("Buffer allocation failed") + .with_context("High memory usage"); + + if let DbError::CapacityExceeded { resource_type, .. } = capacity_error { + assert_eq!( + resource_type, + "High memory usage: Buffer allocation failed: memory" + ); + } else { + panic!("Expected CapacityExceeded error"); + } + } + + #[cfg(feature = "std")] + #[test] + fn test_std_io_error_conversion() { + use std::io::ErrorKind; + + // Test conversion from std::io::Error + let io_error = std::io::Error::new(ErrorKind::NotFound, "File not found"); + let db_error: DbError = io_error.into(); + + if let DbError::Io { source } = db_error { + assert_eq!(source.kind(), ErrorKind::NotFound); + } else { + panic!("Expected Io error variant"); + } + + // Test that the error code is correct for I/O errors + let io_db_error = DbError::Io { + source: std::io::Error::new(ErrorKind::PermissionDenied, "Permission denied"), + }; + assert_eq!(io_db_error.error_code(), 0x8001); + assert_eq!(io_db_error.error_category(), 0x8000); + } + + #[cfg(feature = "std")] + #[test] + fn test_serde_json_error_conversion() { + // Test conversion from serde_json::Error + let invalid_json = r#"{"invalid": json}"#; + let json_error: serde_json::Error = + serde_json::from_str::(invalid_json).unwrap_err(); + let db_error: DbError = json_error.into(); + + if let DbError::Json { .. } = db_error { + // Conversion successful + } else { + panic!("Expected Json error variant"); + } + + // Test that the error code is correct for JSON errors + let json_db_error = DbError::Json { + source: serde_json::from_str::("invalid").unwrap_err(), + }; + assert_eq!(json_db_error.error_code(), 0x9001); + assert_eq!(json_db_error.error_category(), 0x9000); + } + + #[cfg(feature = "std")] + #[test] + fn test_anyhow_integration() { + use anyhow::Context; + + // Test converting DbError to anyhow::Error + let db_error = DbError::network_timeout_with_context(5000, "Connection timeout"); + let anyhow_error: anyhow::Error = db_error.into_anyhow(); + + // Test that the conversion preserves the error message + let error_msg = format!("{}", anyhow_error); + assert!(error_msg.contains("5000ms")); + assert!(error_msg.contains("Connection timeout")); + + // Test error chaining with anyhow + fn failing_db_operation() -> Result<(), DbError> { + Err(DbError::capacity_exceeded_with_type(2048, 1024, "memory")) + } + + let result: anyhow::Result<()> = failing_db_operation() + .map_err(|e| e.into_anyhow()) + .context("Database operation failed") + .context("Application startup error"); + + assert!(result.is_err()); + let error = result.unwrap_err(); + let error_chain = format!("{:#}", error); + assert!(error_chain.contains("Application startup error")); + assert!(error_chain.contains("Database operation failed")); + assert!(error_chain.contains("Capacity exceeded")); + } + + #[cfg(feature = "std")] + #[test] + fn test_std_error_trait_compliance() { + use std::error::Error; + + // Test that DbError implements std::error::Error trait correctly + let timeout_error = DbError::network_timeout_with_context(3000, "Test timeout"); + let error_trait: &dyn Error = &timeout_error; + + // Test Display + let display_msg = format!("{}", error_trait); + assert!(display_msg.contains("3000ms")); + assert!(display_msg.contains("Test timeout")); + + // Test Debug + let debug_msg = format!("{:?}", error_trait); + assert!(debug_msg.contains("NetworkTimeout")); + + // Test error source for errors with sources + let io_error = std::io::Error::other("File not found"); + let db_io_error = DbError::Io { source: io_error }; + let error_trait: &dyn Error = &db_io_error; + + assert!(error_trait.source().is_some()); + let source = error_trait.source().unwrap(); + assert_eq!(source.to_string(), "File not found"); + } + + #[cfg(feature = "std")] + #[test] + fn test_error_source_chaining() { + use std::error::Error; + + // Create a chain: serde_json::Error -> DbError + let json_error = serde_json::from_str::("invalid json").unwrap_err(); + let db_error = DbError::Json { source: json_error }; + + let error_ref: &dyn Error = &db_error; + + // Verify the source chain + assert!(error_ref.source().is_some()); + let source = error_ref.source().unwrap(); + assert!(source.to_string().contains("expected")); + + // Create a chain: io::Error -> DbError + let io_error = std::io::Error::other("Access denied"); + let db_io_error = DbError::Io { source: io_error }; + + let error_ref: &dyn Error = &db_io_error; + assert!(error_ref.source().is_some()); + let source = error_ref.source().unwrap(); + assert_eq!(source.to_string(), "Access denied"); + } + + #[cfg(feature = "std")] + #[test] + fn test_helper_functions() { + // Test that the helper functions work correctly in isolation + let mut empty_string = String::new(); + DbError::prepend_context(&mut empty_string, "New context"); + assert_eq!(empty_string, "New context"); + + let mut existing_string = String::from("existing content"); + DbError::prepend_context(&mut existing_string, "New context"); + assert_eq!(existing_string, "New context: existing content"); + + let mut content_string = String::from("some content"); + DbError::prepend_context_always(&mut content_string, "Always prepend"); + assert_eq!(content_string, "Always prepend: some content"); + } + + #[cfg(feature = "std")] + #[test] + fn test_io_json_context_handling() { + // Test that Io and Json errors can now properly receive context + + // Test I/O error with context + let io_error = std::io::Error::other("File not found"); + let db_io_error = DbError::Io { source: io_error }; + let contextualized = db_io_error.with_context("Reading configuration file"); + + if let DbError::IoWithContext { context, .. } = contextualized { + assert_eq!(context, "Reading configuration file"); + } else { + panic!("Expected IoWithContext error after adding context"); + } + + // Test JSON error with context + let json_error = serde_json::from_str::("invalid json").unwrap_err(); + let db_json_error = DbError::Json { source: json_error }; + let contextualized = db_json_error.with_context("Parsing user input"); + + if let DbError::JsonWithContext { context, .. } = contextualized { + assert_eq!(context, "Parsing user input"); + } else { + panic!("Expected JsonWithContext error after adding context"); + } + + // Test chaining context on already contextualized I/O error + let io_error = std::io::Error::other("Permission denied"); + let io_with_context = DbError::IoWithContext { + context: "Initial context".to_string(), + source: io_error, + }; + let chained = io_with_context.with_context("Additional context"); + + if let DbError::IoWithContext { context, .. } = chained { + assert_eq!(context, "Additional context: Initial context"); + } else { + panic!("Expected IoWithContext error with chained context"); + } + + // Test chaining context on already contextualized JSON error + let json_error = serde_json::from_str::("bad json").unwrap_err(); + let json_with_context = DbError::JsonWithContext { + context: "Initial parse context".to_string(), + source: json_error, + }; + let chained = json_with_context.with_context("Higher level context"); + + if let DbError::JsonWithContext { context, .. } = chained { + assert_eq!(context, "Higher level context: Initial parse context"); + } else { + panic!("Expected JsonWithContext error with chained context"); + } + } + + #[cfg(feature = "std")] + #[test] + fn test_context_error_codes_and_categories() { + // Test that context variants have correct error codes + let io_error = std::io::Error::other("Test error"); + let io_with_context = DbError::IoWithContext { + context: "Test context".to_string(), + source: io_error, + }; + assert_eq!(io_with_context.error_code(), 0x8002); + assert_eq!(io_with_context.error_category(), 0x8000); + + let json_error = serde_json::from_str::("invalid").unwrap_err(); + let json_with_context = DbError::JsonWithContext { + context: "Test context".to_string(), + source: json_error, + }; + assert_eq!(json_with_context.error_code(), 0x9002); + assert_eq!(json_with_context.error_category(), 0x9000); + } + + #[cfg(feature = "std")] + #[test] + fn test_context_error_display() { + // Test that context variants display correctly + let io_error = std::io::Error::other("File not accessible"); + let io_with_context = DbError::IoWithContext { + context: "Loading configuration".to_string(), + source: io_error, + }; + + let display_msg = format!("{}", io_with_context); + assert!(display_msg.contains("Loading configuration")); + assert!(display_msg.contains("File not accessible")); + + let json_error = serde_json::from_str::("malformed").unwrap_err(); + let json_with_context = DbError::JsonWithContext { + context: "Parsing response".to_string(), + source: json_error, + }; + + let display_msg = format!("{}", json_with_context); + assert!(display_msg.contains("Parsing response")); + // The exact JSON error message may vary, so we just check it contains JSON context + assert!(display_msg.contains("JSON error")); + } + + #[cfg(feature = "std")] + #[test] + fn test_context_error_source_preservation() { + use std::error::Error; + + // Test that the original error source is preserved in context variants + let io_error = std::io::Error::other("Original I/O error"); + let io_with_context = DbError::IoWithContext { + context: "Added context".to_string(), + source: io_error, + }; + + let error_trait: &dyn Error = &io_with_context; + let source = error_trait.source().expect("Should have a source error"); + assert_eq!(source.to_string(), "Original I/O error"); + + let json_error = serde_json::from_str::("bad").unwrap_err(); + let json_with_context = DbError::JsonWithContext { + context: "JSON context".to_string(), + source: json_error, + }; + + let error_trait: &dyn Error = &json_with_context; + let source = error_trait.source().expect("Should have a source error"); + assert!(source.to_string().contains("expected")); // Common part of serde_json error messages + } + + #[cfg(feature = "std")] + #[test] + fn test_comprehensive_error_scenarios() { + // Test realistic error scenarios combining all features + + // Scenario 1: Network operation with I/O failure and context + fn simulate_network_failure() -> Result<(), DbError> { + // Simulate file read failure that leads to network error + let io_error = std::io::Error::other("Config file not found"); + Err(DbError::Io { source: io_error }) + } + + let result = simulate_network_failure() + .map_err(|e| e.with_context("Failed to read network configuration")) + .map_err(|e| e.with_context("Network initialization failed")); + + assert!(result.is_err()); + // After our fix, this should now be an IoWithContext error + if let Err(DbError::IoWithContext { context, .. }) = result { + assert!(context.contains("Network initialization failed")); + assert!(context.contains("Failed to read network configuration")); + } else { + panic!("Expected IoWithContext error with proper context chain"); + } + + // Scenario 2: JSON parsing in configuration loading with context + fn parse_config(json: &str) -> Result { + serde_json::from_str(json).map_err(|e| DbError::Json { source: e }) + } + + let result = parse_config(r#"{"invalid": json syntax}"#) + .map_err(|e| e.with_context("Parsing user configuration")) + .map_err(|e| e.with_context("Configuration loading failed")); + + assert!(result.is_err()); + if let Err(DbError::JsonWithContext { context, .. }) = result { + assert!(context.contains("Configuration loading failed")); + assert!(context.contains("Parsing user configuration")); + } else { + panic!("Expected JsonWithContext error with proper context chain"); + } + + // Scenario 3: Converting to anyhow for application boundary + use anyhow::Context; + + fn application_main() -> anyhow::Result<()> { + let db_error = DbError::capacity_exceeded_with_type(1024, 512, "connection pool") + .with_context("Pool exhausted during high load"); + + Err(db_error.into_anyhow()).context("Application failed to start") + } + + let result = application_main(); + assert!(result.is_err()); + let error_msg = format!("{:#}", result.unwrap_err()); + assert!(error_msg.contains("Application failed to start")); + assert!(error_msg.contains("Pool exhausted")); + assert!(error_msg.contains("connection pool")); + } + #[test] fn test_error_category_ranges() { // Verify error category extraction