sz_rust_sdk/
error.rs

1//! Error types for the Senzing Rust SDK
2//!
3//! This module defines a comprehensive error hierarchy that mirrors the
4//! Senzing C# SDK exception hierarchy while leveraging Rust's `Result<T, E>` types.
5//!
6//! The error system provides detailed error information from the underlying
7//! Senzing C library with proper error chains and backtrace support. When errors
8//! occur, the SDK automatically calls the appropriate `getLastException` function
9//! to retrieve detailed error messages from the native library.
10//!
11//! # Error Categories
12//!
13//! * [`SzError::Configuration`] - Configuration and setup errors
14//! * [`SzError::BadInput`] - Invalid input parameters or data
15//! * [`SzError::Database`] - Database connection or operation errors
16//! * [`SzError::NotFound`] - Resource or entity not found
17//! * [`SzError::Retryable`] - Temporary errors that can be retried
18//! * [`SzError::Unrecoverable`] - Fatal errors requiring reinitialization
19//! * [`SzError::License`] - Licensing issues
20//! * [`SzError::NotInitialized`] - System not initialized errors
21//! * [`SzError::DatabaseConnectionLost`] - Database connectivity lost
22//! * [`SzError::DatabaseTransient`] - Temporary database issues
23//! * [`SzError::ReplaceConflict`] - Data replacement conflicts
24//! * [`SzError::RetryTimeoutExceeded`] - Retry timeout exceeded
25//! * [`SzError::Unhandled`] - Unhandled errors
26//! * [`SzError::UnknownDataSource`] - Unknown data source errors
27//! * [`SzError::Unknown`] - Unexpected or unclassified errors
28//!
29//! # Examples
30//!
31//! ```
32//! use sz_rust_sdk::error::{SzError, SzResult};
33//!
34//! fn example_function() -> SzResult<String> {
35//!     // This would normally come from a Senzing operation
36//!     Err(SzError::configuration("Database not initialized"))
37//! }
38//!
39//! match example_function() {
40//!     Ok(result) => println!("Success: {}", result),
41//!     Err(SzError::Configuration { message, .. }) => {
42//!         eprintln!("Configuration error: {}", message);
43//!     }
44//!     Err(e) => eprintln!("Other error: {}", e),
45//! }
46//! ```
47
48use std::ffi::{CStr, NulError};
49
50/// Senzing SDK component for error reporting
51#[derive(Debug, Clone, Copy)]
52pub enum SzComponent {
53    Engine,
54    Config,
55    ConfigMgr,
56    Diagnostic,
57    Product,
58}
59use thiserror::Error;
60
61/// Result type alias for Senzing SDK operations
62///
63/// This is the standard Result type used throughout the Senzing Rust SDK.
64/// All Senzing operations return `SzResult<T>` instead of `Result<T, SzError>`.
65///
66/// # Examples
67///
68/// ```
69/// use sz_rust_sdk::error::SzResult;
70///
71/// fn senzing_operation() -> SzResult<String> {
72///     // Your Senzing operation here
73///     Ok("Success".to_string())
74/// }
75/// ```
76pub type SzResult<T> = Result<T, SzError>;
77
78/// Base error type for all Senzing SDK operations
79///
80/// This enum represents all possible errors that can occur when using the
81/// Senzing SDK. Each variant corresponds to a specific category of error
82/// returned by the underlying Senzing C library.
83///
84/// The error hierarchy is designed to match the Senzing C# SDK for consistency
85/// across language bindings.
86#[derive(Error, Debug)]
87pub enum SzError {
88    /// Errors related to invalid input parameters
89    #[error("Bad input: {message}")]
90    BadInput {
91        message: String,
92        #[source]
93        source: Option<Box<dyn std::error::Error + Send + Sync>>,
94    },
95
96    /// Configuration-related errors
97    #[error("Configuration error: {message}")]
98    Configuration {
99        message: String,
100        #[source]
101        source: Option<Box<dyn std::error::Error + Send + Sync>>,
102    },
103
104    /// Database operation errors
105    #[error("Database error: {message}")]
106    Database {
107        message: String,
108        #[source]
109        source: Option<Box<dyn std::error::Error + Send + Sync>>,
110    },
111
112    /// License-related errors
113    #[error("License error: {message}")]
114    License {
115        message: String,
116        #[source]
117        source: Option<Box<dyn std::error::Error + Send + Sync>>,
118    },
119
120    /// Resource not found errors
121    #[error("Not found: {message}")]
122    NotFound {
123        message: String,
124        #[source]
125        source: Option<Box<dyn std::error::Error + Send + Sync>>,
126    },
127
128    /// Errors that indicate the operation should be retried
129    #[error("Retryable error: {message}")]
130    Retryable {
131        message: String,
132        #[source]
133        source: Option<Box<dyn std::error::Error + Send + Sync>>,
134    },
135
136    /// Unrecoverable errors that require reinitialization
137    #[error("Unrecoverable error: {message}")]
138    Unrecoverable {
139        message: String,
140        #[source]
141        source: Option<Box<dyn std::error::Error + Send + Sync>>,
142    },
143
144    /// Unknown or unexpected errors
145    #[error("Unknown error: {message}")]
146    Unknown {
147        message: String,
148        #[source]
149        source: Option<Box<dyn std::error::Error + Send + Sync>>,
150    },
151
152    /// System not initialized errors
153    #[error("Not initialized: {message}")]
154    NotInitialized {
155        message: String,
156        #[source]
157        source: Option<Box<dyn std::error::Error + Send + Sync>>,
158    },
159
160    /// Database connection lost errors
161    #[error("Database connection lost: {message}")]
162    DatabaseConnectionLost {
163        message: String,
164        #[source]
165        source: Option<Box<dyn std::error::Error + Send + Sync>>,
166    },
167
168    /// Database transient errors
169    #[error("Database transient error: {message}")]
170    DatabaseTransient {
171        message: String,
172        #[source]
173        source: Option<Box<dyn std::error::Error + Send + Sync>>,
174    },
175
176    /// Replace conflict errors
177    #[error("Replace conflict: {message}")]
178    ReplaceConflict {
179        message: String,
180        #[source]
181        source: Option<Box<dyn std::error::Error + Send + Sync>>,
182    },
183
184    /// Retry timeout exceeded errors
185    #[error("Retry timeout exceeded: {message}")]
186    RetryTimeoutExceeded {
187        message: String,
188        #[source]
189        source: Option<Box<dyn std::error::Error + Send + Sync>>,
190    },
191
192    /// Unhandled errors
193    #[error("Unhandled error: {message}")]
194    Unhandled {
195        message: String,
196        #[source]
197        source: Option<Box<dyn std::error::Error + Send + Sync>>,
198    },
199
200    /// Unknown data source errors
201    #[error("Unknown data source: {message}")]
202    UnknownDataSource {
203        message: String,
204        #[source]
205        source: Option<Box<dyn std::error::Error + Send + Sync>>,
206    },
207
208    /// FFI-related errors
209    #[error("FFI error: {message}")]
210    Ffi {
211        message: String,
212        #[source]
213        source: Option<Box<dyn std::error::Error + Send + Sync>>,
214    },
215
216    /// JSON serialization/deserialization errors
217    #[error("JSON error: {0}")]
218    Json(#[from] serde_json::Error),
219
220    /// String conversion errors (C string handling)
221    #[error("String conversion error: {0}")]
222    StringConversion(#[from] NulError),
223}
224
225impl SzError {
226    /// Creates a new BadInput error
227    pub fn bad_input<S: Into<String>>(message: S) -> Self {
228        Self::BadInput {
229            message: message.into(),
230            source: None,
231        }
232    }
233
234    /// Creates a new Configuration error
235    pub fn configuration<S: Into<String>>(message: S) -> Self {
236        Self::Configuration {
237            message: message.into(),
238            source: None,
239        }
240    }
241
242    /// Creates a new Database error
243    pub fn database<S: Into<String>>(message: S) -> Self {
244        Self::Database {
245            message: message.into(),
246            source: None,
247        }
248    }
249
250    /// Creates a new License error
251    pub fn license<S: Into<String>>(message: S) -> Self {
252        Self::License {
253            message: message.into(),
254            source: None,
255        }
256    }
257
258    /// Creates a new NotFound error
259    pub fn not_found<S: Into<String>>(message: S) -> Self {
260        Self::NotFound {
261            message: message.into(),
262            source: None,
263        }
264    }
265
266    /// Creates a new Retryable error
267    pub fn retryable<S: Into<String>>(message: S) -> Self {
268        Self::Retryable {
269            message: message.into(),
270            source: None,
271        }
272    }
273
274    /// Creates a new Unrecoverable error
275    pub fn unrecoverable<S: Into<String>>(message: S) -> Self {
276        Self::Unrecoverable {
277            message: message.into(),
278            source: None,
279        }
280    }
281
282    /// Creates a new Unknown error
283    pub fn unknown<S: Into<String>>(message: S) -> Self {
284        Self::Unknown {
285            message: message.into(),
286            source: None,
287        }
288    }
289
290    /// Creates a new FFI error
291    pub fn ffi<S: Into<String>>(message: S) -> Self {
292        Self::Ffi {
293            message: message.into(),
294            source: None,
295        }
296    }
297
298    /// Creates a new NotInitialized error
299    pub fn not_initialized<S: Into<String>>(message: S) -> Self {
300        Self::NotInitialized {
301            message: message.into(),
302            source: None,
303        }
304    }
305
306    /// Creates a new DatabaseConnectionLost error
307    pub fn database_connection_lost<S: Into<String>>(message: S) -> Self {
308        Self::DatabaseConnectionLost {
309            message: message.into(),
310            source: None,
311        }
312    }
313
314    /// Creates a new DatabaseTransient error
315    pub fn database_transient<S: Into<String>>(message: S) -> Self {
316        Self::DatabaseTransient {
317            message: message.into(),
318            source: None,
319        }
320    }
321
322    /// Creates a new ReplaceConflict error
323    pub fn replace_conflict<S: Into<String>>(message: S) -> Self {
324        Self::ReplaceConflict {
325            message: message.into(),
326            source: None,
327        }
328    }
329
330    /// Creates a new RetryTimeoutExceeded error
331    pub fn retry_timeout_exceeded<S: Into<String>>(message: S) -> Self {
332        Self::RetryTimeoutExceeded {
333            message: message.into(),
334            source: None,
335        }
336    }
337
338    /// Creates a new Unhandled error
339    pub fn unhandled<S: Into<String>>(message: S) -> Self {
340        Self::Unhandled {
341            message: message.into(),
342            source: None,
343        }
344    }
345
346    /// Creates a new UnknownDataSource error
347    pub fn unknown_data_source<S: Into<String>>(message: S) -> Self {
348        Self::UnknownDataSource {
349            message: message.into(),
350            source: None,
351        }
352    }
353
354    /// Returns true if this error indicates the operation should be retried
355    pub fn is_retryable(&self) -> bool {
356        matches!(self, SzError::Retryable { .. })
357    }
358
359    /// Returns true if this error is unrecoverable
360    pub fn is_unrecoverable(&self) -> bool {
361        matches!(self, SzError::Unrecoverable { .. })
362    }
363
364    /// Creates an error from getLastExceptionCode() with message from getLastException()
365    pub fn from_code_with_message(error_code: i64, component: SzComponent) -> Self {
366        let error_msg = Self::get_last_exception_message(component, error_code);
367
368        match error_code {
369            // Specific error codes that map to new error types (check these first)
370            47..=63 => Self::not_initialized(error_msg), // Not initialized errors
371
372            // Detailed error code ranges from getLastExceptionCode()
373            0..=46 | 64..=100 => Self::bad_input(error_msg), // Bad input range (excluding not_initialized)
374            999 => Self::license(error_msg),                 // License error
375            1000..=1020 => Self::database(error_msg),        // Database errors
376            2000..=2300 => Self::configuration(error_msg),   // Configuration errors
377            7200..=7299 => Self::configuration(error_msg),   // Configuration errors (extended)
378            7301..=7400 => Self::bad_input(error_msg),       // Bad input errors (extended)
379            8500..=8600 => Self::database(error_msg),        // Secure storage/database
380            9000..=9099 | 9201..=9999 => Self::license(error_msg), // License errors (extended)
381            9100..=9200 => Self::configuration(error_msg),   // Configuration errors (extended)
382
383            // Default to unknown for any other codes
384            _ => Self::unknown(error_msg),
385        }
386    }
387
388    /// Gets the last exception message from the specified component
389    fn get_last_exception_message(component: SzComponent, error_code: i64) -> String {
390        use crate::ffi;
391        use libc::c_char;
392
393        const BUFFER_SIZE: usize = 4096;
394        let mut buffer = vec![0i8; BUFFER_SIZE];
395
396        let result = unsafe {
397            match component {
398                SzComponent::Engine => ffi::bindings::Sz_getLastException(
399                    buffer.as_mut_ptr() as *mut c_char,
400                    BUFFER_SIZE as i64,
401                ),
402                SzComponent::Config => ffi::bindings::SzConfig_getLastException(
403                    buffer.as_mut_ptr() as *mut c_char,
404                    BUFFER_SIZE as i64,
405                ),
406                SzComponent::ConfigMgr => ffi::bindings::SzConfigMgr_getLastException(
407                    buffer.as_mut_ptr() as *mut c_char,
408                    BUFFER_SIZE as i64,
409                ),
410                SzComponent::Diagnostic => ffi::bindings::SzDiagnostic_getLastException(
411                    buffer.as_mut_ptr() as *mut c_char,
412                    BUFFER_SIZE as i64,
413                ),
414                SzComponent::Product => ffi::bindings::SzProduct_getLastException(
415                    buffer.as_mut_ptr() as *mut c_char,
416                    BUFFER_SIZE as i64,
417                ),
418            }
419        };
420
421        if result > 0 {
422            // Successfully got exception message
423            unsafe {
424                match CStr::from_ptr(buffer.as_ptr()).to_str() {
425                    Ok(message) if !message.is_empty() => message.to_string(),
426                    _ => format!("Native error (code: {})", error_code),
427                }
428            }
429        } else {
430            // Failed to get exception message, use generic message
431            format!("Native error (code: {})", error_code)
432        }
433    }
434
435    /// Creates an error from getLastExceptionCode() (legacy method for compatibility)
436    pub fn from_code(error_code: i64) -> Self {
437        // Default to Engine component for backward compatibility
438        Self::from_code_with_message(error_code, SzComponent::Engine)
439    }
440
441    /// Creates an Unknown error from a source error
442    pub fn from_source(source: Box<dyn std::error::Error + Send + Sync>) -> Self {
443        Self::Unknown {
444            message: source.to_string(),
445            source: Some(source),
446        }
447    }
448
449    /// Creates an Unknown error with a custom message and source
450    pub fn with_message_and_source<S: Into<String>>(
451        message: S,
452        source: Box<dyn std::error::Error + Send + Sync>,
453    ) -> Self {
454        Self::Unknown {
455            message: message.into(),
456            source: Some(source),
457        }
458    }
459}
460
461/// Utility function to convert C string errors to SzError (Internal)
462///
463/// # Safety
464///
465/// The caller must ensure that `c_str` is either null or points to a valid null-terminated C string.
466#[allow(dead_code)]
467pub(crate) unsafe fn c_str_to_sz_error(c_str: *const i8) -> SzError {
468    if c_str.is_null() {
469        return SzError::unknown("Unknown error occurred");
470    }
471
472    match unsafe { CStr::from_ptr(c_str) }.to_str() {
473        Ok(error_msg) => SzError::unknown(error_msg),
474        Err(_) => SzError::ffi("Failed to convert C string to Rust string"),
475    }
476}
477
478#[cfg(test)]
479mod test_error_mapping {
480    use super::*;
481
482    #[test]
483    fn test_error_code_7220_maps_to_configuration() {
484        let error = SzError::from_code(7220);
485        match error {
486            SzError::Configuration { message, .. } => {
487                // Message should either be from getLastException or fallback format
488                assert!(message.contains("7220") || !message.is_empty());
489            }
490            _ => panic!(
491                "Error code 7220 should map to Configuration, got: {:?}",
492                error
493            ),
494        }
495    }
496
497    #[test]
498    fn test_not_initialized_error_codes() {
499        for code in 47..=63 {
500            let error = SzError::from_code(code);
501            match error {
502                SzError::NotInitialized { .. } => {
503                    // Expected
504                }
505                _ => panic!(
506                    "Error code {} should map to NotInitialized, got: {:?}",
507                    code, error
508                ),
509            }
510        }
511    }
512
513    #[test]
514    fn test_license_error_code_999() {
515        let error = SzError::from_code(999);
516        match error {
517            SzError::License { .. } => {
518                // Expected
519            }
520            _ => panic!("Error code 999 should map to License, got: {:?}", error),
521        }
522    }
523
524    #[test]
525    fn test_database_error_range() {
526        let error = SzError::from_code(1010);
527        match error {
528            SzError::Database { .. } => {
529                // Expected
530            }
531            _ => panic!("Error code 1010 should map to Database, got: {:?}", error),
532        }
533    }
534
535    #[test]
536    fn test_unknown_error_default() {
537        let error = SzError::from_code(99999);
538        match error {
539            SzError::Unknown { .. } => {
540                // Expected
541            }
542            _ => panic!("Error code 99999 should map to Unknown, got: {:?}", error),
543        }
544    }
545
546    #[test]
547    fn test_from_code_with_message() {
548        let error = SzError::from_code_with_message(7220, SzComponent::Config);
549        match error {
550            SzError::Configuration { message, .. } => {
551                // Message should either be from getLastException or fallback format
552                assert!(!message.is_empty());
553            }
554            _ => panic!(
555                "Error code 7220 should map to Configuration, got: {:?}",
556                error
557            ),
558        }
559    }
560}