sz_rust_sdk/
error.rs

1//! Handling errors from the Senzing SDK.
2//!
3//! Every SDK method returns [`SzResult<T>`], which is `Result<T, SzError>`.
4//! When an operation fails, the SDK maps the native Senzing error code to the
5//! appropriate [`SzError`] variant and includes the original error message.
6//!
7//! # Deciding what to do with an error
8//!
9//! The error hierarchy tells you how to respond:
10//!
11//! * **Retryable** — temporary failure; the same call may succeed if you
12//!   retry after a brief delay.
13//!   * [`DatabaseConnectionLost`](SzError::DatabaseConnectionLost) — connection dropped
14//!   * [`DatabaseTransient`](SzError::DatabaseTransient) — deadlock, lock timeout
15//!   * [`RetryTimeoutExceeded`](SzError::RetryTimeoutExceeded) — internal retry budget exhausted
16//! * **Bad input** — the caller supplied invalid data; fix the request and try again.
17//!   * [`NotFound`](SzError::NotFound) — entity or record does not exist
18//!   * [`UnknownDataSource`](SzError::UnknownDataSource) — data source not registered
19//! * **Unrecoverable** — the SDK is in a broken state; reinitialize.
20//!   * [`Database`](SzError::Database) — permanent database failure
21//!   * [`License`](SzError::License) — license expired or invalid
22//!   * [`NotInitialized`](SzError::NotInitialized) — SDK not yet initialized
23//!   * [`Unhandled`](SzError::Unhandled) — unexpected internal error
24//! * **Configuration** — fix the configuration and reinitialize.
25//! * **ReplaceConflict** — the default config was changed by another process.
26//!
27//! # Handling errors from Senzing calls
28//!
29//! ## Quick classification
30//!
31//! Use the boolean helpers to branch on error category:
32//!
33//! ```no_run
34//! use sz_rust_sdk::prelude::*;
35//! use std::thread;
36//! use std::time::Duration;
37//!
38//! # fn example(engine: &dyn SzEngine) -> SzResult<()> {
39//! let record = r#"{"NAME_FULL": "John Smith"}"#;
40//! match engine.add_record("CUSTOMERS", "1", record, None) {
41//!     Ok(info) => println!("Added: {info}"),
42//!     Err(ref e) if e.is_retryable() => {
43//!         eprintln!("Temporary failure, retrying: {e}");
44//!         thread::sleep(Duration::from_secs(1));
45//!     }
46//!     Err(ref e) if e.is_bad_input() => {
47//!         eprintln!("Bad input, skipping record: {e}");
48//!     }
49//!     Err(e) => return Err(e),  // propagate everything else
50//! }
51//! # Ok(())
52//! # }
53//! ```
54//!
55//! ## Pattern matching on specific variants
56//!
57//! ```no_run
58//! use sz_rust_sdk::prelude::*;
59//!
60//! # fn example(engine: &dyn SzEngine) -> SzResult<()> {
61//! match engine.get_record("CUSTOMERS", "CUST001", None) {
62//!     Ok(json) => println!("{json}"),
63//!     Err(SzError::NotFound(_)) => println!("Record does not exist"),
64//!     Err(SzError::UnknownDataSource(_)) => println!("Data source not registered"),
65//!     Err(e) => return Err(e),
66//! }
67//! # Ok(())
68//! # }
69//! ```
70//!
71//! ## Polymorphic category checking with `ErrorCategory`
72//!
73//! Every error belongs to a hierarchy. [`SzError::is()`] checks whether
74//! the error matches a category **or any of its subtypes**, so you can
75//! write broad handlers without listing every variant:
76//!
77//! ```no_run
78//! use sz_rust_sdk::prelude::*;
79//!
80//! # fn example(err: &SzError) {
81//! // DatabaseTransient matches both its own category and the parent Retryable
82//! let err = SzError::database_transient("Deadlock");
83//! assert!(err.is(ErrorCategory::DatabaseTransient));
84//! assert!(err.is(ErrorCategory::Retryable));
85//! # }
86//! ```
87//!
88//! ## Retry loop with backoff
89//!
90//! ```no_run
91//! use sz_rust_sdk::prelude::*;
92//! use std::thread;
93//! use std::time::Duration;
94//!
95//! fn add_with_retry(
96//!     engine: &dyn SzEngine,
97//!     json: &str,
98//!     max_retries: u32,
99//! ) -> SzResult<String> {
100//!     let mut attempt = 0;
101//!     loop {
102//!         match engine.add_record("CUSTOMERS", "1", json, None) {
103//!             Ok(info) => return Ok(info),
104//!             Err(ref e) if e.is_retryable() && attempt < max_retries => {
105//!                 attempt += 1;
106//!                 eprintln!("Retry {attempt}/{max_retries}: {e}");
107//!                 thread::sleep(Duration::from_millis(100 * 2u64.pow(attempt)));
108//!             }
109//!             Err(e) => return Err(e),
110//!         }
111//!     }
112//! }
113//! ```
114//!
115//! ## Inspecting error details
116//!
117//! Every [`SzError`] carries the native Senzing error code and message:
118//!
119//! ```no_run
120//! use sz_rust_sdk::prelude::*;
121//!
122//! fn log_senzing_error(err: &SzError) {
123//!     eprintln!("Category: {}", err.category());
124//!     eprintln!("Severity: {}", err.severity());
125//!     eprintln!("Message:  {}", err.message());
126//!     if let Some(code) = err.error_code() {
127//!         eprintln!("Native code: {code}");
128//!     }
129//! }
130//! ```
131//!
132//! # Handling Senzing errors inside mixed-error functions
133//!
134//! When a function calls both Senzing and non-Senzing operations (file I/O,
135//! JSON parsing, HTTP, etc.), Rust's `?` operator needs a single error type
136//! for the return — typically `Result<T, Box<dyn Error>>` or a custom enum.
137//! The [`SzErrorInspect`] trait (automatically implemented for all error
138//! types) walks the error chain to find and inspect any embedded `SzError`:
139//!
140//! ```no_run
141//! use sz_rust_sdk::prelude::*;
142//! use std::fs;
143//!
144//! fn load_from_file(
145//!     engine: &dyn SzEngine,
146//!     path: &str,
147//! ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
148//!     let data = fs::read_to_string(path)?;         // io::Error on failure
149//!     engine.add_record("TEST", "1", &data, None)?;  // SzError on failure
150//!     Ok(())
151//! }
152//!
153//! # fn main() {
154//! # let engine: Box<dyn SzEngine> = todo!();
155//! match load_from_file(&*engine, "data.json") {
156//!     Ok(()) => {}
157//!     Err(ref e) if e.is_sz_retryable() => eprintln!("Retry: {e}"),
158//!     Err(ref e) if e.is_sz_bad_input() => eprintln!("Bad input: {e}"),
159//!     Err(ref e) if e.is_sz_unrecoverable() => eprintln!("Fatal: {e}"),
160//!     Err(e) => eprintln!("Other error: {e}"),
161//! }
162//! # }
163//! ```
164//!
165//! Use [`sz_error()`](SzErrorInspect::sz_error) to extract the underlying
166//! `SzError` when you need full details:
167//!
168//! ```no_run
169//! use sz_rust_sdk::prelude::*;
170//!
171//! fn log_error(err: &(dyn std::error::Error + 'static)) {
172//!     match err.sz_error() {
173//!         Some(sz) => {
174//!             eprintln!("[{}] {}", sz.category(), sz.message());
175//!             if let Some(code) = sz.error_code() {
176//!                 eprintln!("  native code: {code}");
177//!             }
178//!         }
179//!         None => eprintln!("Non-Senzing error: {err}"),
180//!     }
181//! }
182//! ```
183
184use std::ffi::{CStr, NulError};
185
186/// Senzing SDK component for error reporting
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum SzComponent {
189    Engine,
190    Config,
191    ConfigMgr,
192    Diagnostic,
193    Product,
194}
195
196/// Error categories for hierarchy-based error handling.
197///
198/// Use these with [`SzError::is()`] or [`SzErrorInspect::is_sz()`] for
199/// polymorphic error checking. The hierarchy means a `DatabaseTransient`
200/// error matches both `ErrorCategory::DatabaseTransient` (specific) and
201/// `ErrorCategory::Retryable` (parent). Check specific categories first,
202/// then broader ones.
203///
204/// # Examples
205///
206/// ```no_run
207/// use sz_rust_sdk::prelude::*;
208///
209/// # fn example(engine: &dyn SzEngine) {
210/// if let Err(e) = engine.add_record("TEST", "1", "{}", None) {
211///     if e.is(ErrorCategory::DatabaseTransient) {
212///         eprintln!("Transient database issue, retry immediately");
213///     } else if e.is(ErrorCategory::Retryable) {
214///         eprintln!("Retryable error, retry with backoff");
215///     } else if e.is(ErrorCategory::NotFound) {
216///         eprintln!("Entity/record not found");
217///     }
218/// }
219/// # }
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum ErrorCategory {
222    // Base categories
223    BadInput,
224    Retryable,
225    Unrecoverable,
226
227    // Specific types under BadInput
228    NotFound,
229    UnknownDataSource,
230
231    // Specific types under Retryable
232    DatabaseConnectionLost,
233    DatabaseTransient,
234    RetryTimeoutExceeded,
235
236    // Specific types under Unrecoverable
237    Database,
238    License,
239    NotInitialized,
240    Unhandled,
241
242    // Standalone types
243    Configuration,
244    ReplaceConflict,
245    EnvironmentDestroyed,
246    Unknown,
247}
248
249/// Error context carried by each [`SzError`] variant.
250///
251/// Every `SzError` you receive from an SDK call contains an `ErrorContext`
252/// with details from the native Senzing library. You access these through
253/// the convenience methods on `SzError` itself — [`error_code()`](SzError::error_code),
254/// [`message()`](SzError::message), [`component()`](SzError::component) — rather
255/// than reading `ErrorContext` fields directly.
256#[derive(Debug)]
257pub struct ErrorContext {
258    /// Human-readable error message
259    pub message: String,
260    /// Optional Senzing native error code
261    pub code: Option<i64>,
262    /// Optional SDK component that generated the error
263    pub component: Option<SzComponent>,
264    /// Optional underlying cause of this error
265    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
266}
267
268impl ErrorContext {
269    /// Creates a new ErrorContext with just a message
270    pub fn new<S: Into<String>>(message: S) -> Self {
271        Self {
272            message: message.into(),
273            code: None,
274            component: None,
275            source: None,
276        }
277    }
278
279    /// Creates an ErrorContext with message, code, and component
280    pub fn with_code<S: Into<String>>(message: S, code: i64, component: SzComponent) -> Self {
281        Self {
282            message: message.into(),
283            code: Some(code),
284            component: Some(component),
285            source: None,
286        }
287    }
288
289    /// Adds a source error to this context
290    pub fn with_source<E>(mut self, source: E) -> Self
291    where
292        E: std::error::Error + Send + Sync + 'static,
293    {
294        self.source = Some(Box::new(source));
295        self
296    }
297}
298
299impl std::fmt::Display for ErrorContext {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        write!(f, "{}", self.message)?;
302        if let Some(code) = self.code {
303            write!(f, " (code: {})", code)?;
304        }
305        Ok(())
306    }
307}
308
309/// Result type alias for Senzing SDK operations
310///
311/// This is the standard Result type used throughout the Senzing Rust SDK.
312/// All Senzing operations return `SzResult<T>` instead of `Result<T, SzError>`.
313///
314/// # Examples
315///
316/// ```no_run
317/// use sz_rust_sdk::error::SzResult;
318///
319/// fn senzing_operation() -> SzResult<String> {
320///     // Your Senzing operation here
321///     Ok("Success".to_string())
322/// }
323/// ```
324pub type SzResult<T> = Result<T, SzError>;
325
326/// Extension trait for [`SzResult<T>`] providing error classification helpers.
327///
328/// These methods let you handle retryable errors inline without explicit
329/// match arms. They operate on `SzResult` (i.e., `Result<T, SzError>`)
330/// returned by SDK calls.
331///
332/// # Examples
333///
334/// ```no_run
335/// use sz_rust_sdk::prelude::*;
336///
337/// # fn example(engine: &dyn SzEngine) -> SzResult<String> {
338/// engine.add_record("TEST", "1", r#"{"NAME_FULL":"Test"}"#, None)
339///     .or_retry(|e| {
340///         eprintln!("Retrying due to: {e}");
341///         engine.add_record("TEST", "1", r#"{"NAME_FULL":"Test"}"#, None)
342///     })
343/// # }
344/// ```
345pub trait SzResultExt<T> {
346    /// If the error is retryable, call the provided closure; otherwise propagate the error.
347    ///
348    /// # Examples
349    ///
350    /// ```no_run
351    /// use sz_rust_sdk::prelude::*;
352    ///
353    /// # fn example(engine: &dyn SzEngine) -> SzResult<String> {
354    /// engine.add_record("TEST", "1", "{}", None)
355    ///     .or_retry(|e| {
356    ///         eprintln!("Retrying: {e}");
357    ///         engine.add_record("TEST", "1", "{}", None)
358    ///     })
359    /// # }
360    /// ```
361    fn or_retry<F>(self, f: F) -> SzResult<T>
362    where
363        F: FnOnce(SzError) -> SzResult<T>;
364
365    /// Maps retryable errors using the provided function, propagates non-retryable errors.
366    ///
367    /// # Examples
368    ///
369    /// ```no_run
370    /// use sz_rust_sdk::prelude::*;
371    ///
372    /// # fn example(engine: &dyn SzEngine) -> SzResult<String> {
373    /// engine.add_record("TEST", "1", "{}", None)
374    ///     .map_retryable(|e| {
375    ///         eprintln!("Will retry: {e}");
376    ///         engine.add_record("TEST", "1", "{}", None)
377    ///     })
378    /// # }
379    /// ```
380    fn map_retryable<F>(self, f: F) -> SzResult<T>
381    where
382        F: FnOnce(SzError) -> SzResult<T>;
383
384    /// Returns `Ok(None)` for retryable errors, `Err` for non-retryable errors.
385    ///
386    /// Useful for filtering retryable errors out of a processing loop.
387    ///
388    /// # Examples
389    ///
390    /// ```no_run
391    /// use sz_rust_sdk::prelude::*;
392    ///
393    /// # fn example(engine: &dyn SzEngine) {
394    /// match engine.add_record("TEST", "1", "{}", None).filter_retryable() {
395    ///     Ok(Some(info)) => println!("Success: {info}"),
396    ///     Ok(None) => println!("Retryable error, will retry"),
397    ///     Err(e) => println!("Fatal error: {e}"),
398    /// }
399    /// # }
400    /// ```
401    fn filter_retryable(self) -> Result<Option<T>, SzError>;
402
403    /// Returns true if the result is an error and that error is retryable
404    fn is_retryable_error(&self) -> bool;
405
406    /// Returns true if the result is an error and that error is unrecoverable
407    fn is_unrecoverable_error(&self) -> bool;
408
409    /// Returns true if the result is an error and that error is bad input
410    fn is_bad_input_error(&self) -> bool;
411}
412
413impl<T> SzResultExt<T> for SzResult<T> {
414    fn or_retry<F>(self, f: F) -> SzResult<T>
415    where
416        F: FnOnce(SzError) -> SzResult<T>,
417    {
418        match self {
419            Ok(value) => Ok(value),
420            Err(e) if e.is_retryable() => f(e),
421            Err(e) => Err(e),
422        }
423    }
424
425    fn map_retryable<F>(self, f: F) -> SzResult<T>
426    where
427        F: FnOnce(SzError) -> SzResult<T>,
428    {
429        self.or_retry(f)
430    }
431
432    fn filter_retryable(self) -> Result<Option<T>, SzError> {
433        match self {
434            Ok(value) => Ok(Some(value)),
435            Err(e) if e.is_retryable() => Ok(None),
436            Err(e) => Err(e),
437        }
438    }
439
440    fn is_retryable_error(&self) -> bool {
441        matches!(self, Err(e) if e.is_retryable())
442    }
443
444    fn is_unrecoverable_error(&self) -> bool {
445        matches!(self, Err(e) if e.is_unrecoverable())
446    }
447
448    fn is_bad_input_error(&self) -> bool {
449        matches!(self, Err(e) if e.is_bad_input())
450    }
451}
452
453/// Inspect any error chain for an embedded [`SzError`].
454///
455/// Automatically implemented for every type that implements
456/// `std::error::Error + 'static`, including `Box<dyn Error>`,
457/// `anyhow::Error`, and custom error enums. The methods walk the
458/// [`source()`](std::error::Error::source) chain, find the first
459/// `SzError` (if any), and expose its classification — no manual
460/// downcasting required.
461///
462/// # Examples
463///
464/// ## Senzing-only functions
465///
466/// When every call returns `SzResult`, you can use the native methods
467/// directly. `SzErrorInspect` also works here (it finds the `SzError`
468/// immediately since there is no wrapping), but the native methods
469/// are equivalent:
470///
471/// ```no_run
472/// use sz_rust_sdk::prelude::*;
473/// use std::thread;
474/// use std::time::Duration;
475///
476/// fn add_with_retry(
477///     engine: &dyn SzEngine,
478///     record_json: &str,
479///     max_retries: u32,
480/// ) -> SzResult<String> {
481///     let mut attempt = 0;
482///     loop {
483///         match engine.add_record("CUSTOMERS", "1", record_json, None) {
484///             Ok(info) => return Ok(info),
485///             Err(ref e) if e.is_retryable() && attempt < max_retries => {
486///                 attempt += 1;
487///                 let delay = Duration::from_millis(100 * 2u64.pow(attempt));
488///                 eprintln!("Retryable (attempt {attempt}/{max_retries}): {e}");
489///                 thread::sleep(delay);
490///             }
491///             Err(e) => return Err(e),
492///         }
493///     }
494/// }
495/// ```
496///
497/// ## Functions that mix Senzing with other error types
498///
499/// When a function calls both Senzing and non-Senzing operations, Rust's
500/// `?` operator needs a single error type for the return. The standard
501/// approach is `Result<T, Box<dyn Error>>` (or `anyhow::Result<T>`, or a
502/// custom enum — whatever the application already uses). `SzErrorInspect`
503/// works on all of them:
504///
505/// ```no_run
506/// use sz_rust_sdk::prelude::*;
507/// use std::fs;
508///
509/// /// Load records from a JSON file into the Senzing repository.
510/// fn load_from_file(
511///     engine: &dyn SzEngine,
512///     path: &str,
513/// ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
514///     let data = fs::read_to_string(path)?;         // io::Error on failure
515///     let records: Vec<serde_json::Value> =
516///         serde_json::from_str(&data)?;              // serde::Error on failure
517///
518///     for record in &records {
519///         let id = record["RECORD_ID"].as_str().unwrap_or("unknown");
520///         let json = serde_json::to_string(record)?; // serde::Error
521///         engine.add_record("CUSTOMERS", id, &json, None)?; // SzError
522///     }
523///     Ok(())
524/// }
525///
526/// # fn main() {
527/// # let engine: Box<dyn SzEngine> = todo!();
528/// // At the call site, SzErrorInspect methods work on any error in the chain.
529/// // They return false for io::Error, serde::Error, or anything that isn't
530/// // a Senzing error.
531/// match load_from_file(&*engine, "records.json") {
532///     Ok(()) => println!("All records loaded"),
533///     Err(ref e) if e.is_sz_retryable() => {
534///         eprintln!("Transient Senzing error, retry: {e}");
535///     }
536///     Err(ref e) if e.is_sz(ErrorCategory::NotFound) => {
537///         eprintln!("Entity not found: {e}");
538///     }
539///     Err(ref e) if e.is_sz_unrecoverable() => {
540///         eprintln!("Unrecoverable Senzing error: {e}");
541///     }
542///     Err(e) => eprintln!("Error: {e}"),
543/// }
544/// # }
545/// ```
546///
547/// ## Extracting the `SzError` for detailed inspection
548///
549/// Use [`sz_error()`](SzErrorInspect::sz_error) to get the underlying
550/// `SzError` reference, then inspect its error code, message, severity,
551/// or full category hierarchy:
552///
553/// ```no_run
554/// use sz_rust_sdk::prelude::*;
555///
556/// fn handle_error(err: &(dyn std::error::Error + 'static)) {
557///     match err.sz_error() {
558///         Some(sz) => {
559///             eprintln!("Senzing error [{}]: {}", sz.category(), sz.message());
560///             eprintln!("  Severity: {}", sz.severity());
561///             if let Some(code) = sz.error_code() {
562///                 eprintln!("  Native code: {code}");
563///             }
564///         }
565///         None => eprintln!("Non-Senzing error: {err}"),
566///     }
567/// }
568/// ```
569///
570/// ## Granular classification with `is_sz(ErrorCategory)`
571///
572/// [`is_sz()`](SzErrorInspect::is_sz) checks the full hierarchy — a
573/// `DatabaseTransient` error matches both `ErrorCategory::DatabaseTransient`
574/// and its parent `ErrorCategory::Retryable`. Check specific types first,
575/// then broader categories:
576///
577/// ```no_run
578/// use sz_rust_sdk::prelude::*;
579///
580/// fn classify(err: &(dyn std::error::Error + 'static)) -> &'static str {
581///     if err.is_sz(ErrorCategory::DatabaseConnectionLost) {
582///         "database connection lost — check connectivity"
583///     } else if err.is_sz(ErrorCategory::DatabaseTransient) {
584///         "transient database issue — retry immediately"
585///     } else if err.is_sz(ErrorCategory::Retryable) {
586///         "retryable — retry with backoff"
587///     } else if err.is_sz(ErrorCategory::NotFound) {
588///         "entity not found — check record ID"
589///     } else if err.is_sz(ErrorCategory::BadInput) {
590///         "invalid input — fix request data"
591///     } else if err.is_sz(ErrorCategory::License) {
592///         "license error — check Senzing license"
593///     } else if err.is_sz(ErrorCategory::Unrecoverable) {
594///         "unrecoverable — reinitialize the SDK"
595///     } else {
596///         "non-Senzing error"
597///     }
598/// }
599/// ```
600///
601/// ## Custom error enum
602///
603/// Any error type that implements `std::error::Error` and returns the
604/// inner `SzError` from `source()` works automatically:
605///
606/// ```no_run
607/// use sz_rust_sdk::prelude::*;
608///
609/// #[derive(Debug)]
610/// enum AppError {
611///     Senzing(SzError),
612///     Io(std::io::Error),
613/// }
614///
615/// impl std::fmt::Display for AppError {
616///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617///         match self {
618///             Self::Senzing(e) => write!(f, "senzing: {e}"),
619///             Self::Io(e) => write!(f, "io: {e}"),
620///         }
621///     }
622/// }
623///
624/// impl std::error::Error for AppError {
625///     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
626///         match self {
627///             Self::Senzing(e) => Some(e),
628///             Self::Io(e) => Some(e),
629///         }
630///     }
631/// }
632///
633/// let err = AppError::Senzing(SzError::database_transient("Deadlock"));
634/// assert!(err.is_sz_retryable());
635/// assert!(err.is_sz(ErrorCategory::DatabaseTransient));
636///
637/// let err = AppError::Io(std::io::Error::new(
638///     std::io::ErrorKind::NotFound, "file missing"
639/// ));
640/// assert!(!err.is_sz_retryable());
641/// assert!(err.sz_error().is_none());
642/// ```
643pub trait SzErrorInspect {
644    /// Returns a reference to the first [`SzError`] found in the error chain,
645    /// or `None` if no `SzError` is present.
646    ///
647    /// This is the foundation method that all other `SzErrorInspect` methods
648    /// build upon. Use it when you need direct access to the `SzError` for
649    /// detailed inspection (error code, message, component, hierarchy).
650    ///
651    /// # Examples
652    ///
653    /// ```no_run
654    /// use sz_rust_sdk::prelude::*;
655    ///
656    /// let err = SzError::license("License expired");
657    /// let boxed: Box<dyn std::error::Error + Send + Sync> = Box::new(err);
658    ///
659    /// // Extract the SzError from the Box
660    /// let sz = boxed.sz_error().expect("should contain an SzError");
661    /// assert!(sz.is_license());
662    /// assert_eq!(sz.severity(), "critical");
663    /// ```
664    fn sz_error(&self) -> Option<&SzError>;
665
666    /// Returns `true` if the chain contains a retryable [`SzError`].
667    ///
668    /// Retryable errors are temporary failures where the same operation may
669    /// succeed if attempted again. This includes:
670    /// - [`SzError::Retryable`] — generic retryable error
671    /// - [`SzError::DatabaseConnectionLost`] — database connection dropped
672    /// - [`SzError::DatabaseTransient`] — deadlocks, lock timeouts, etc.
673    /// - [`SzError::RetryTimeoutExceeded`] — retry budget exhausted
674    ///
675    /// Returns `false` if no `SzError` exists in the chain, or if the
676    /// `SzError` is not retryable.
677    ///
678    /// # Examples
679    ///
680    /// ```no_run
681    /// use sz_rust_sdk::prelude::*;
682    ///
683    /// // Retryable Senzing error
684    /// let err = SzError::database_transient("Deadlock");
685    /// assert!(err.is_sz_retryable());
686    ///
687    /// // Non-retryable Senzing error
688    /// let err = SzError::not_found("Entity 42");
689    /// assert!(!err.is_sz_retryable());
690    ///
691    /// // Non-Senzing error
692    /// let err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broke");
693    /// assert!(!err.is_sz_retryable());
694    /// ```
695    fn is_sz_retryable(&self) -> bool {
696        self.sz_error().is_some_and(|e| e.is_retryable())
697    }
698
699    /// Returns `true` if the chain contains an unrecoverable [`SzError`].
700    ///
701    /// Unrecoverable errors indicate the SDK is in a broken state and
702    /// typically requires reinitialization. This includes:
703    /// - [`SzError::Unrecoverable`] — generic unrecoverable error
704    /// - [`SzError::Database`] — permanent database failure (schema errors, corruption)
705    /// - [`SzError::License`] — license expired or invalid
706    /// - [`SzError::NotInitialized`] — SDK not initialized
707    /// - [`SzError::Unhandled`] — unexpected internal error
708    ///
709    /// Returns `false` if no `SzError` exists in the chain, or if the
710    /// `SzError` is not unrecoverable.
711    ///
712    /// # Examples
713    ///
714    /// ```no_run
715    /// use sz_rust_sdk::prelude::*;
716    ///
717    /// let err = SzError::license("License expired");
718    /// assert!(err.is_sz_unrecoverable());
719    ///
720    /// let err = SzError::database_transient("Deadlock");
721    /// assert!(!err.is_sz_unrecoverable());  // retryable, not unrecoverable
722    /// ```
723    fn is_sz_unrecoverable(&self) -> bool {
724        self.sz_error().is_some_and(|e| e.is_unrecoverable())
725    }
726
727    /// Returns `true` if the chain contains a bad-input [`SzError`].
728    ///
729    /// Bad input errors indicate the caller provided invalid data. This
730    /// includes:
731    /// - [`SzError::BadInput`] — generic invalid input
732    /// - [`SzError::NotFound`] — entity or record not found
733    /// - [`SzError::UnknownDataSource`] — unregistered data source name
734    ///
735    /// Returns `false` if no `SzError` exists in the chain, or if the
736    /// `SzError` is not a bad-input error.
737    ///
738    /// # Examples
739    ///
740    /// ```no_run
741    /// use sz_rust_sdk::prelude::*;
742    ///
743    /// let err = SzError::unknown_data_source("FAKE_SOURCE");
744    /// assert!(err.is_sz_bad_input());
745    ///
746    /// let err = SzError::configuration("Bad config");
747    /// assert!(!err.is_sz_bad_input());  // configuration, not bad input
748    /// ```
749    fn is_sz_bad_input(&self) -> bool {
750        self.sz_error().is_some_and(|e| e.is_bad_input())
751    }
752
753    /// Returns `true` if the chain contains an [`SzError`] matching the
754    /// given [`ErrorCategory`].
755    ///
756    /// This is the most flexible inspection method. It delegates to
757    /// [`SzError::is()`], which checks the full error hierarchy. A
758    /// `DatabaseTransient` error matches both `ErrorCategory::DatabaseTransient`
759    /// (its specific type) and `ErrorCategory::Retryable` (its parent category).
760    ///
761    /// Returns `false` if no `SzError` exists in the chain, or if the
762    /// `SzError` does not match the category.
763    ///
764    /// # Available categories
765    ///
766    /// | Category | Parent | Matches |
767    /// |---|---|---|
768    /// | `BadInput` | — | `BadInput`, `NotFound`, `UnknownDataSource` |
769    /// | `NotFound` | `BadInput` | `NotFound` only |
770    /// | `UnknownDataSource` | `BadInput` | `UnknownDataSource` only |
771    /// | `Retryable` | — | `Retryable`, `DatabaseConnectionLost`, `DatabaseTransient`, `RetryTimeoutExceeded` |
772    /// | `DatabaseConnectionLost` | `Retryable` | `DatabaseConnectionLost` only |
773    /// | `DatabaseTransient` | `Retryable` | `DatabaseTransient` only |
774    /// | `RetryTimeoutExceeded` | `Retryable` | `RetryTimeoutExceeded` only |
775    /// | `Unrecoverable` | — | `Unrecoverable`, `Database`, `License`, `NotInitialized`, `Unhandled` |
776    /// | `Database` | `Unrecoverable` | `Database` only |
777    /// | `License` | `Unrecoverable` | `License` only |
778    /// | `NotInitialized` | `Unrecoverable` | `NotInitialized` only |
779    /// | `Unhandled` | `Unrecoverable` | `Unhandled` only |
780    /// | `Configuration` | — | `Configuration` only |
781    /// | `ReplaceConflict` | — | `ReplaceConflict` only |
782    /// | `EnvironmentDestroyed` | — | `EnvironmentDestroyed` only |
783    /// | `Unknown` | — | `Unknown` only |
784    ///
785    /// # Examples
786    ///
787    /// ```no_run
788    /// use sz_rust_sdk::prelude::*;
789    ///
790    /// let err = SzError::database_transient("Deadlock");
791    ///
792    /// // Exact match
793    /// assert!(err.is_sz(ErrorCategory::DatabaseTransient));
794    ///
795    /// // Parent category match
796    /// assert!(err.is_sz(ErrorCategory::Retryable));
797    ///
798    /// // Not in this hierarchy
799    /// assert!(!err.is_sz(ErrorCategory::BadInput));
800    /// assert!(!err.is_sz(ErrorCategory::Unrecoverable));
801    /// ```
802    fn is_sz(&self, category: ErrorCategory) -> bool {
803        self.sz_error().is_some_and(|e| e.is(category))
804    }
805}
806
807impl<E: std::error::Error + 'static> SzErrorInspect for E {
808    fn sz_error(&self) -> Option<&SzError> {
809        SzError::find_in_chain(self)
810    }
811}
812
813impl SzErrorInspect for dyn std::error::Error + 'static {
814    fn sz_error(&self) -> Option<&SzError> {
815        SzError::find_in_chain(self)
816    }
817}
818
819impl SzErrorInspect for dyn std::error::Error + Send + 'static {
820    fn sz_error(&self) -> Option<&SzError> {
821        SzError::find_in_chain(self)
822    }
823}
824
825impl SzErrorInspect for dyn std::error::Error + Send + Sync + 'static {
826    fn sz_error(&self) -> Option<&SzError> {
827        SzError::find_in_chain(self)
828    }
829}
830
831/// The error type returned by all Senzing SDK operations.
832///
833/// Every SDK method returns [`SzResult<T>`], which is `Result<T, SzError>`.
834/// Each variant maps to a specific category of failure from the native
835/// Senzing library — see the [module documentation](crate::error) for
836/// a guide to handling these errors.
837///
838/// # Handling errors
839///
840/// You can match on specific variants, use the boolean classification
841/// methods ([`is_retryable()`](SzError::is_retryable),
842/// [`is_bad_input()`](SzError::is_bad_input), etc.), or use
843/// [`is()`](SzError::is) for polymorphic hierarchy checks.
844///
845/// This enum is `#[non_exhaustive]`, so always include a catch-all arm:
846///
847/// # Examples
848///
849/// ```no_run
850/// use sz_rust_sdk::prelude::*;
851///
852/// # fn example(engine: &dyn SzEngine) -> SzResult<()> {
853/// match engine.get_record("CUSTOMERS", "CUST001", None) {
854///     Ok(json) => println!("{json}"),
855///     Err(SzError::NotFound(_)) => println!("Record does not exist"),
856///     Err(SzError::UnknownDataSource(_)) => println!("Data source not registered"),
857///     Err(e) if e.is_retryable() => println!("Temporary failure, retry: {e}"),
858///     // Always include catch-all for non-exhaustive enums
859///     Err(e) => return Err(e),
860/// }
861/// # Ok(())
862/// # }
863#[derive(Debug)]
864#[non_exhaustive]
865pub enum SzError {
866    /// Errors related to invalid input parameters
867    BadInput(ErrorContext),
868
869    /// Configuration-related errors
870    Configuration(ErrorContext),
871
872    /// Database operation errors
873    Database(ErrorContext),
874
875    /// License-related errors
876    License(ErrorContext),
877
878    /// Resource not found errors
879    NotFound(ErrorContext),
880
881    /// Errors that indicate the operation should be retried
882    Retryable(ErrorContext),
883
884    /// Unrecoverable errors that require reinitialization
885    Unrecoverable(ErrorContext),
886
887    /// Unknown or unexpected errors
888    Unknown(ErrorContext),
889
890    /// System not initialized errors
891    NotInitialized(ErrorContext),
892
893    /// Database connection lost errors
894    DatabaseConnectionLost(ErrorContext),
895
896    /// Database transient errors (e.g., deadlocks)
897    DatabaseTransient(ErrorContext),
898
899    /// Replace conflict errors
900    ReplaceConflict(ErrorContext),
901
902    /// Retry timeout exceeded errors
903    RetryTimeoutExceeded(ErrorContext),
904
905    /// Unhandled errors
906    Unhandled(ErrorContext),
907
908    /// Unknown data source errors
909    UnknownDataSource(ErrorContext),
910
911    /// Environment has been destroyed
912    ///
913    /// Corresponds to SzEnvironmentDestroyedException in C# SDK.
914    /// This error occurs when attempting to use an environment that has already
915    /// been destroyed.
916    EnvironmentDestroyed(ErrorContext),
917
918    /// FFI-related errors
919    Ffi(ErrorContext),
920
921    /// JSON serialization/deserialization errors
922    Json(serde_json::Error),
923
924    /// String conversion errors (C string handling)
925    StringConversion(NulError),
926}
927
928// Manual implementation of Display and Error traits
929impl std::fmt::Display for SzError {
930    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
931        match self {
932            Self::BadInput(ctx) => write!(f, "Bad input: {}", ctx),
933            Self::Configuration(ctx) => write!(f, "Configuration error: {}", ctx),
934            Self::Database(ctx) => write!(f, "Database error: {}", ctx),
935            Self::License(ctx) => write!(f, "License error: {}", ctx),
936            Self::NotFound(ctx) => write!(f, "Not found: {}", ctx),
937            Self::Retryable(ctx) => write!(f, "Retryable error: {}", ctx),
938            Self::Unrecoverable(ctx) => write!(f, "Unrecoverable error: {}", ctx),
939            Self::Unknown(ctx) => write!(f, "Unknown error: {}", ctx),
940            Self::NotInitialized(ctx) => write!(f, "Not initialized: {}", ctx),
941            Self::DatabaseConnectionLost(ctx) => write!(f, "Database connection lost: {}", ctx),
942            Self::DatabaseTransient(ctx) => write!(f, "Database transient error: {}", ctx),
943            Self::ReplaceConflict(ctx) => write!(f, "Replace conflict: {}", ctx),
944            Self::RetryTimeoutExceeded(ctx) => write!(f, "Retry timeout exceeded: {}", ctx),
945            Self::Unhandled(ctx) => write!(f, "Unhandled error: {}", ctx),
946            Self::UnknownDataSource(ctx) => write!(f, "Unknown data source: {}", ctx),
947            Self::EnvironmentDestroyed(ctx) => write!(f, "Environment destroyed: {}", ctx),
948            Self::Ffi(ctx) => write!(f, "FFI error: {}", ctx),
949            Self::Json(e) => write!(f, "JSON error: {}", e),
950            Self::StringConversion(e) => write!(f, "String conversion error: {}", e),
951        }
952    }
953}
954
955impl std::error::Error for SzError {
956    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
957        match self {
958            Self::BadInput(ctx)
959            | Self::Configuration(ctx)
960            | Self::Database(ctx)
961            | Self::License(ctx)
962            | Self::NotFound(ctx)
963            | Self::Retryable(ctx)
964            | Self::Unrecoverable(ctx)
965            | Self::Unknown(ctx)
966            | Self::NotInitialized(ctx)
967            | Self::DatabaseConnectionLost(ctx)
968            | Self::DatabaseTransient(ctx)
969            | Self::ReplaceConflict(ctx)
970            | Self::RetryTimeoutExceeded(ctx)
971            | Self::Unhandled(ctx)
972            | Self::UnknownDataSource(ctx)
973            | Self::EnvironmentDestroyed(ctx)
974            | Self::Ffi(ctx) => ctx.source.as_ref().map(|e| &**e as &dyn std::error::Error),
975            Self::Json(e) => Some(e),
976            Self::StringConversion(e) => Some(e),
977        }
978    }
979}
980
981// Implement From for automatic conversions
982impl From<serde_json::Error> for SzError {
983    fn from(err: serde_json::Error) -> Self {
984        Self::Json(err)
985    }
986}
987
988impl From<NulError> for SzError {
989    fn from(err: NulError) -> Self {
990        Self::StringConversion(err)
991    }
992}
993
994impl SzError {
995    // ========================================================================
996    // Error Construction
997    //
998    // These constructors are used internally by the SDK to create errors
999    // from native Senzing error codes. They are public for use in tests
1000    // and edge cases, but SDK consumers typically receive errors from
1001    // SDK method calls rather than constructing them directly.
1002    // ========================================================================
1003
1004    /// Creates a new BadInput error
1005    pub fn bad_input<S: Into<String>>(message: S) -> Self {
1006        Self::BadInput(ErrorContext::new(message))
1007    }
1008
1009    /// Creates a new Configuration error
1010    pub fn configuration<S: Into<String>>(message: S) -> Self {
1011        Self::Configuration(ErrorContext::new(message))
1012    }
1013
1014    /// Creates a new Database error
1015    pub fn database<S: Into<String>>(message: S) -> Self {
1016        Self::Database(ErrorContext::new(message))
1017    }
1018
1019    /// Creates a new License error
1020    pub fn license<S: Into<String>>(message: S) -> Self {
1021        Self::License(ErrorContext::new(message))
1022    }
1023
1024    /// Creates a new NotFound error
1025    pub fn not_found<S: Into<String>>(message: S) -> Self {
1026        Self::NotFound(ErrorContext::new(message))
1027    }
1028
1029    /// Creates a new Retryable error
1030    pub fn retryable<S: Into<String>>(message: S) -> Self {
1031        Self::Retryable(ErrorContext::new(message))
1032    }
1033
1034    /// Creates a new Unrecoverable error
1035    pub fn unrecoverable<S: Into<String>>(message: S) -> Self {
1036        Self::Unrecoverable(ErrorContext::new(message))
1037    }
1038
1039    /// Creates a new Unknown error
1040    pub fn unknown<S: Into<String>>(message: S) -> Self {
1041        Self::Unknown(ErrorContext::new(message))
1042    }
1043
1044    /// Creates a new FFI error
1045    pub fn ffi<S: Into<String>>(message: S) -> Self {
1046        Self::Ffi(ErrorContext::new(message))
1047    }
1048
1049    /// Creates a new NotInitialized error
1050    pub fn not_initialized<S: Into<String>>(message: S) -> Self {
1051        Self::NotInitialized(ErrorContext::new(message))
1052    }
1053
1054    /// Creates a new DatabaseConnectionLost error
1055    pub fn database_connection_lost<S: Into<String>>(message: S) -> Self {
1056        Self::DatabaseConnectionLost(ErrorContext::new(message))
1057    }
1058
1059    /// Creates a new DatabaseTransient error
1060    pub fn database_transient<S: Into<String>>(message: S) -> Self {
1061        Self::DatabaseTransient(ErrorContext::new(message))
1062    }
1063
1064    /// Creates a new ReplaceConflict error
1065    pub fn replace_conflict<S: Into<String>>(message: S) -> Self {
1066        Self::ReplaceConflict(ErrorContext::new(message))
1067    }
1068
1069    /// Creates a new RetryTimeoutExceeded error
1070    pub fn retry_timeout_exceeded<S: Into<String>>(message: S) -> Self {
1071        Self::RetryTimeoutExceeded(ErrorContext::new(message))
1072    }
1073
1074    /// Creates a new Unhandled error
1075    pub fn unhandled<S: Into<String>>(message: S) -> Self {
1076        Self::Unhandled(ErrorContext::new(message))
1077    }
1078
1079    /// Creates a new UnknownDataSource error
1080    pub fn unknown_data_source<S: Into<String>>(message: S) -> Self {
1081        Self::UnknownDataSource(ErrorContext::new(message))
1082    }
1083
1084    /// Creates a new EnvironmentDestroyed error
1085    pub fn environment_destroyed<S: Into<String>>(message: S) -> Self {
1086        Self::EnvironmentDestroyed(ErrorContext::new(message))
1087    }
1088
1089    // ========================================================================
1090    // Error Chain Inspection - Static Methods
1091    // ========================================================================
1092
1093    /// Finds the first [`SzError`] in an error's [`source()`](std::error::Error::source) chain.
1094    ///
1095    /// Checks the error itself first via `downcast_ref`,
1096    /// then iteratively walks the `source()` chain. Returns `None` if no
1097    /// `SzError` is found anywhere in the chain.
1098    ///
1099    /// # When to use this vs `SzErrorInspect`
1100    ///
1101    /// In most cases, prefer the [`SzErrorInspect`] trait methods
1102    /// (`is_sz_retryable()`, `sz_error()`, etc.) — they call this internally
1103    /// and provide a cleaner API. Use `find_in_chain` directly when you have
1104    /// a bare `&dyn Error` reference and the trait is not in scope, or in
1105    /// generic contexts where the trait bound is awkward.
1106    ///
1107    /// # Examples
1108    ///
1109    /// ```no_run
1110    /// use sz_rust_sdk::error::SzError;
1111    ///
1112    /// // No SzError in an io::Error
1113    /// let io_err: Box<dyn std::error::Error> =
1114    ///     Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, "missing"));
1115    /// assert!(SzError::find_in_chain(io_err.as_ref()).is_none());
1116    ///
1117    /// // SzError found directly
1118    /// let sz_err: Box<dyn std::error::Error> =
1119    ///     Box::new(SzError::database_transient("Deadlock"));
1120    /// let found = SzError::find_in_chain(sz_err.as_ref()).unwrap();
1121    /// assert!(found.is_retryable());
1122    /// assert_eq!(found.category(), "database_transient");
1123    /// ```
1124    pub fn find_in_chain<'a>(err: &'a (dyn std::error::Error + 'static)) -> Option<&'a SzError> {
1125        // Check the error itself
1126        if let Some(sz) = err.downcast_ref::<SzError>() {
1127            return Some(sz);
1128        }
1129        // Walk the source chain
1130        let mut source = err.source();
1131        while let Some(err) = source {
1132            if let Some(sz) = err.downcast_ref::<SzError>() {
1133                return Some(sz);
1134            }
1135            source = err.source();
1136        }
1137        None
1138    }
1139
1140    // ========================================================================
1141    // Error Inspection - Helper Methods
1142    // ========================================================================
1143
1144    /// Returns the native Senzing error code, if available.
1145    ///
1146    /// Most errors returned by the SDK carry the numeric code from
1147    /// `getLastExceptionCode()`. Use this for logging, metrics, or
1148    /// when you need to look up a specific code in the Senzing
1149    /// documentation.
1150    ///
1151    /// # Examples
1152    ///
1153    /// ```no_run
1154    /// use sz_rust_sdk::prelude::*;
1155    ///
1156    /// # fn example(engine: &dyn SzEngine) {
1157    /// if let Err(e) = engine.add_record("TEST", "1", "{}", None) {
1158    ///     if let Some(code) = e.error_code() {
1159    ///         eprintln!("Senzing error code {code}: {e}");
1160    ///     }
1161    /// }
1162    /// # }
1163    pub fn error_code(&self) -> Option<i64> {
1164        match self {
1165            Self::BadInput(ctx)
1166            | Self::Configuration(ctx)
1167            | Self::Database(ctx)
1168            | Self::License(ctx)
1169            | Self::NotFound(ctx)
1170            | Self::Retryable(ctx)
1171            | Self::Unrecoverable(ctx)
1172            | Self::Unknown(ctx)
1173            | Self::NotInitialized(ctx)
1174            | Self::DatabaseConnectionLost(ctx)
1175            | Self::DatabaseTransient(ctx)
1176            | Self::ReplaceConflict(ctx)
1177            | Self::RetryTimeoutExceeded(ctx)
1178            | Self::Unhandled(ctx)
1179            | Self::UnknownDataSource(ctx)
1180            | Self::EnvironmentDestroyed(ctx)
1181            | Self::Ffi(ctx) => ctx.code,
1182            Self::Json(_) | Self::StringConversion(_) => None,
1183        }
1184    }
1185
1186    /// Returns the SDK component that generated this error.
1187    ///
1188    /// Indicates which Senzing subsystem (Engine, Config, ConfigMgr,
1189    /// Diagnostic, Product) produced the error. Useful for targeted
1190    /// logging or diagnostics.
1191    ///
1192    /// # Examples
1193    ///
1194    /// ```no_run
1195    /// use sz_rust_sdk::prelude::*;
1196    /// use sz_rust_sdk::error::SzComponent;
1197    ///
1198    /// # fn example(engine: &dyn SzEngine) {
1199    /// if let Err(e) = engine.add_record("TEST", "1", "{}", None) {
1200    ///     if let Some(component) = e.component() {
1201    ///         eprintln!("Error from {:?}: {e}", component);
1202    ///     }
1203    /// }
1204    /// # }
1205    pub fn component(&self) -> Option<SzComponent> {
1206        match self {
1207            Self::BadInput(ctx)
1208            | Self::Configuration(ctx)
1209            | Self::Database(ctx)
1210            | Self::License(ctx)
1211            | Self::NotFound(ctx)
1212            | Self::Retryable(ctx)
1213            | Self::Unrecoverable(ctx)
1214            | Self::Unknown(ctx)
1215            | Self::NotInitialized(ctx)
1216            | Self::DatabaseConnectionLost(ctx)
1217            | Self::DatabaseTransient(ctx)
1218            | Self::ReplaceConflict(ctx)
1219            | Self::RetryTimeoutExceeded(ctx)
1220            | Self::Unhandled(ctx)
1221            | Self::UnknownDataSource(ctx)
1222            | Self::EnvironmentDestroyed(ctx)
1223            | Self::Ffi(ctx) => ctx.component,
1224            Self::Json(_) | Self::StringConversion(_) => None,
1225        }
1226    }
1227
1228    /// Returns the error message without the error type prefix.
1229    ///
1230    /// This gives you the raw message from the native Senzing library,
1231    /// without the "Bad input: " or "Database error: " prefix that
1232    /// [`Display`](std::fmt::Display) adds.
1233    ///
1234    /// # Examples
1235    ///
1236    /// ```no_run
1237    /// use sz_rust_sdk::prelude::*;
1238    ///
1239    /// # fn example(engine: &dyn SzEngine) {
1240    /// if let Err(e) = engine.get_record("TEST", "MISSING", None) {
1241    ///     eprintln!("message: {}", e.message());
1242    ///     eprintln!("display: {e}");  // includes type prefix
1243    /// }
1244    /// # }
1245    pub fn message(&self) -> &str {
1246        match self {
1247            Self::BadInput(ctx)
1248            | Self::Configuration(ctx)
1249            | Self::Database(ctx)
1250            | Self::License(ctx)
1251            | Self::NotFound(ctx)
1252            | Self::Retryable(ctx)
1253            | Self::Unrecoverable(ctx)
1254            | Self::Unknown(ctx)
1255            | Self::NotInitialized(ctx)
1256            | Self::DatabaseConnectionLost(ctx)
1257            | Self::DatabaseTransient(ctx)
1258            | Self::ReplaceConflict(ctx)
1259            | Self::RetryTimeoutExceeded(ctx)
1260            | Self::Unhandled(ctx)
1261            | Self::UnknownDataSource(ctx)
1262            | Self::EnvironmentDestroyed(ctx)
1263            | Self::Ffi(ctx) => &ctx.message,
1264            Self::Json(_) => "JSON error",
1265            Self::StringConversion(_) => "String conversion error",
1266        }
1267    }
1268
1269    /// Returns true if this error indicates the operation should be retried
1270    ///
1271    /// This includes Retryable and its subtypes:
1272    /// - DatabaseConnectionLost
1273    /// - DatabaseTransient
1274    /// - RetryTimeoutExceeded
1275    ///
1276    /// # Examples
1277    ///
1278    /// ```no_run
1279    /// use sz_rust_sdk::error::SzError;
1280    ///
1281    /// let error = SzError::database_connection_lost("Connection lost");
1282    /// assert!(error.is_retryable());
1283    /// ```
1284    pub fn is_retryable(&self) -> bool {
1285        matches!(
1286            self,
1287            SzError::Retryable(_)
1288                | SzError::DatabaseConnectionLost(_)
1289                | SzError::DatabaseTransient(_)
1290                | SzError::RetryTimeoutExceeded(_)
1291        )
1292    }
1293
1294    /// Returns true if this error is unrecoverable
1295    ///
1296    /// This includes Unrecoverable and its subtypes:
1297    /// - Database
1298    /// - License
1299    /// - NotInitialized
1300    /// - Unhandled
1301    ///
1302    /// # Examples
1303    ///
1304    /// ```no_run
1305    /// use sz_rust_sdk::error::SzError;
1306    ///
1307    /// let error = SzError::license("License expired");
1308    /// assert!(error.is_unrecoverable());
1309    /// ```
1310    pub fn is_unrecoverable(&self) -> bool {
1311        matches!(
1312            self,
1313            SzError::Unrecoverable(_)
1314                | SzError::Database(_)
1315                | SzError::License(_)
1316                | SzError::NotInitialized(_)
1317                | SzError::Unhandled(_)
1318        )
1319    }
1320
1321    /// Returns true if this error is a bad input error
1322    ///
1323    /// This includes BadInput and its subtypes:
1324    /// - NotFound
1325    /// - UnknownDataSource
1326    ///
1327    /// # Examples
1328    ///
1329    /// ```no_run
1330    /// use sz_rust_sdk::error::SzError;
1331    ///
1332    /// let error = SzError::not_found("Entity not found");
1333    /// assert!(error.is_bad_input());
1334    /// ```
1335    pub fn is_bad_input(&self) -> bool {
1336        matches!(
1337            self,
1338            SzError::BadInput(_) | SzError::NotFound(_) | SzError::UnknownDataSource(_)
1339        )
1340    }
1341
1342    /// Returns true if this is a database-related error
1343    ///
1344    /// This includes ALL database errors regardless of retryability:
1345    /// - Database (unrecoverable)
1346    /// - DatabaseConnectionLost (retryable)
1347    /// - DatabaseTransient (retryable)
1348    ///
1349    /// # Examples
1350    ///
1351    /// ```no_run
1352    /// use sz_rust_sdk::error::SzError;
1353    ///
1354    /// // Unrecoverable database error
1355    /// let error = SzError::database("Schema error");
1356    /// assert!(error.is_database());
1357    /// assert!(error.is_unrecoverable());
1358    ///
1359    /// // Retryable database error
1360    /// let error = SzError::database_transient("Deadlock");
1361    /// assert!(error.is_database());
1362    /// assert!(error.is_retryable());
1363    /// ```
1364    pub fn is_database(&self) -> bool {
1365        matches!(
1366            self,
1367            SzError::Database(_)
1368                | SzError::DatabaseConnectionLost(_)
1369                | SzError::DatabaseTransient(_)
1370        )
1371    }
1372
1373    /// Returns true if this is a license-related error
1374    ///
1375    /// # Examples
1376    ///
1377    /// ```no_run
1378    /// use sz_rust_sdk::error::SzError;
1379    ///
1380    /// let error = SzError::license("License expired");
1381    /// assert!(error.is_license());
1382    /// ```
1383    pub fn is_license(&self) -> bool {
1384        matches!(self, SzError::License(_))
1385    }
1386
1387    /// Returns true if this is a configuration-related error
1388    ///
1389    /// # Examples
1390    ///
1391    /// ```no_run
1392    /// use sz_rust_sdk::error::SzError;
1393    ///
1394    /// let error = SzError::configuration("Invalid config");
1395    /// assert!(error.is_configuration());
1396    /// ```
1397    pub fn is_configuration(&self) -> bool {
1398        matches!(self, SzError::Configuration(_))
1399    }
1400
1401    /// Returns true if this is an initialization-related error
1402    ///
1403    /// # Examples
1404    ///
1405    /// ```no_run
1406    /// use sz_rust_sdk::error::SzError;
1407    ///
1408    /// let error = SzError::not_initialized("SDK not initialized");
1409    /// assert!(error.is_initialization());
1410    /// ```
1411    pub fn is_initialization(&self) -> bool {
1412        matches!(self, SzError::NotInitialized(_))
1413    }
1414
1415    /// Returns this error's type hierarchy from most specific to least
1416    ///
1417    /// This makes parent-child relationships explicit and queryable at runtime.
1418    /// The first element is always the most specific type, followed by parent
1419    /// categories in order.
1420    ///
1421    /// # Examples
1422    ///
1423    /// ```no_run
1424    /// use sz_rust_sdk::error::{SzError, ErrorCategory};
1425    ///
1426    /// let err = SzError::database_transient("Deadlock");
1427    ///
1428    /// // Get the full hierarchy
1429    /// let hierarchy = err.hierarchy();
1430    /// assert_eq!(hierarchy, vec![
1431    ///     ErrorCategory::DatabaseTransient,
1432    ///     ErrorCategory::Retryable,
1433    /// ]);
1434    ///
1435    /// // Check if error "is a" Retryable (polymorphic check)
1436    /// assert!(err.is(ErrorCategory::Retryable));
1437    /// assert!(err.is(ErrorCategory::DatabaseTransient));
1438    /// ```
1439    pub fn hierarchy(&self) -> Vec<ErrorCategory> {
1440        // If we have an error code, use the generated hierarchy
1441        if let Some(code) = self.error_code() {
1442            let generated = crate::error_mappings_generated::get_error_hierarchy(code);
1443            if !generated.is_empty() {
1444                return generated;
1445            }
1446        }
1447
1448        // Fallback to manual mapping for errors without codes
1449        match self {
1450            // BadInput family
1451            Self::BadInput(_) => vec![ErrorCategory::BadInput],
1452            Self::NotFound(_) => vec![ErrorCategory::NotFound, ErrorCategory::BadInput],
1453            Self::UnknownDataSource(_) => {
1454                vec![ErrorCategory::UnknownDataSource, ErrorCategory::BadInput]
1455            }
1456
1457            // Retryable family
1458            Self::Retryable(_) => vec![ErrorCategory::Retryable],
1459            Self::DatabaseConnectionLost(_) => {
1460                vec![
1461                    ErrorCategory::DatabaseConnectionLost,
1462                    ErrorCategory::Retryable,
1463                ]
1464            }
1465            Self::DatabaseTransient(_) => {
1466                vec![ErrorCategory::DatabaseTransient, ErrorCategory::Retryable]
1467            }
1468            Self::RetryTimeoutExceeded(_) => {
1469                vec![
1470                    ErrorCategory::RetryTimeoutExceeded,
1471                    ErrorCategory::Retryable,
1472                ]
1473            }
1474
1475            // Unrecoverable family
1476            Self::Unrecoverable(_) => vec![ErrorCategory::Unrecoverable],
1477            Self::Database(_) => vec![ErrorCategory::Database, ErrorCategory::Unrecoverable],
1478            Self::License(_) => vec![ErrorCategory::License, ErrorCategory::Unrecoverable],
1479            Self::NotInitialized(_) => {
1480                vec![ErrorCategory::NotInitialized, ErrorCategory::Unrecoverable]
1481            }
1482            Self::Unhandled(_) => vec![ErrorCategory::Unhandled, ErrorCategory::Unrecoverable],
1483
1484            // Standalone types
1485            Self::Configuration(_) => vec![ErrorCategory::Configuration],
1486            Self::ReplaceConflict(_) => vec![ErrorCategory::ReplaceConflict],
1487            Self::EnvironmentDestroyed(_) => vec![ErrorCategory::EnvironmentDestroyed],
1488            Self::Unknown(_) => vec![ErrorCategory::Unknown],
1489
1490            // FFI errors (no hierarchy)
1491            Self::Ffi(_) | Self::Json(_) | Self::StringConversion(_) => vec![],
1492        }
1493    }
1494
1495    /// Checks if this error belongs to a category (polymorphic check)
1496    ///
1497    /// This checks the entire hierarchy, so `DatabaseTransient` will return
1498    /// true for both `ErrorCategory::DatabaseTransient` and `ErrorCategory::Retryable`.
1499    ///
1500    /// # Examples
1501    ///
1502    /// ```no_run
1503    /// use sz_rust_sdk::error::{SzError, ErrorCategory};
1504    ///
1505    /// let err = SzError::database_transient("Deadlock");
1506    ///
1507    /// // Check specific type
1508    /// assert!(err.is(ErrorCategory::DatabaseTransient));
1509    ///
1510    /// // Check parent category (polymorphic)
1511    /// assert!(err.is(ErrorCategory::Retryable));
1512    ///
1513    /// // Not in this category
1514    /// assert!(!err.is(ErrorCategory::BadInput));
1515    /// ```
1516    pub fn is(&self, category: ErrorCategory) -> bool {
1517        self.hierarchy().contains(&category)
1518    }
1519
1520    // ========================================================================
1521    // Error Metadata - For Error Reporting Integration
1522    // ========================================================================
1523
1524    /// Returns the error category as a string.
1525    ///
1526    /// Useful for structured logging, metrics, and error reporting systems
1527    /// that categorize errors by type.
1528    ///
1529    /// # Examples
1530    ///
1531    /// ```no_run
1532    /// use sz_rust_sdk::prelude::*;
1533    ///
1534    /// # fn example(engine: &dyn SzEngine) {
1535    /// if let Err(e) = engine.add_record("TEST", "1", "{}", None) {
1536    ///     eprintln!("[{}] {}", e.category(), e);
1537    ///     // e.g. "[bad_input] Bad input: ..."
1538    /// }
1539    /// # }
1540    pub fn category(&self) -> &'static str {
1541        match self {
1542            Self::BadInput(_) | Self::NotFound(_) | Self::UnknownDataSource(_) => "bad_input",
1543            Self::Configuration(_) => "configuration",
1544            Self::Database(_) => "database",
1545            Self::DatabaseConnectionLost(_) => "database_connection",
1546            Self::DatabaseTransient(_) => "database_transient",
1547            Self::License(_) => "license",
1548            Self::NotInitialized(_) => "not_initialized",
1549            Self::Retryable(_) | Self::RetryTimeoutExceeded(_) => "retryable",
1550            Self::Unrecoverable(_) | Self::Unhandled(_) => "unrecoverable",
1551            Self::ReplaceConflict(_) => "replace_conflict",
1552            Self::EnvironmentDestroyed(_) => "environment_destroyed",
1553            Self::Unknown(_) => "unknown",
1554            Self::Ffi(_) => "ffi",
1555            Self::Json(_) => "json",
1556            Self::StringConversion(_) => "string_conversion",
1557        }
1558    }
1559
1560    /// Returns the severity level of this error.
1561    ///
1562    /// Severity levels:
1563    /// - `"critical"`: License failures, unhandled errors
1564    /// - `"high"`: Database errors, not initialized
1565    /// - `"medium"`: Connection issues, transient errors, configuration
1566    /// - `"low"`: Input validation, not found
1567    ///
1568    /// # Examples
1569    ///
1570    /// ```no_run
1571    /// use sz_rust_sdk::prelude::*;
1572    ///
1573    /// # fn example(engine: &dyn SzEngine) {
1574    /// if let Err(e) = engine.add_record("TEST", "1", "{}", None) {
1575    ///     eprintln!("[{}:{}] {}", e.severity(), e.category(), e);
1576    /// }
1577    /// # }
1578    pub fn severity(&self) -> &'static str {
1579        match self {
1580            Self::License(_) | Self::Unrecoverable(_) | Self::Unhandled(_) => "critical",
1581            Self::Database(_) | Self::NotInitialized(_) => "high",
1582            Self::DatabaseConnectionLost(_)
1583            | Self::DatabaseTransient(_)
1584            | Self::Configuration(_) => "medium",
1585            _ => "low",
1586        }
1587    }
1588
1589    // ========================================================================
1590    // Error Code Mapping - From Native Senzing Errors
1591    // ========================================================================
1592
1593    /// Creates an error from getLastExceptionCode() with message from getLastException()
1594    ///
1595    /// This method maps native Senzing error codes to the appropriate Rust error type.
1596    /// The mapping is auto-generated from szerrors.json and covers all 456 Senzing error codes.
1597    ///
1598    /// # Examples
1599    ///
1600    /// ```no_run
1601    /// use sz_rust_sdk::error::{SzError, SzComponent};
1602    ///
1603    /// let error = SzError::from_code_with_message(999, SzComponent::Engine);
1604    /// assert!(matches!(error, SzError::License(_)));
1605    /// assert_eq!(error.error_code(), Some(999));
1606    /// ```
1607    pub fn from_code_with_message(error_code: i64, component: SzComponent) -> Self {
1608        let error_msg = Self::get_last_exception_message(component, error_code);
1609        let ctx = ErrorContext::with_code(error_msg, error_code, component);
1610
1611        // Use generated error mapping (456 error codes from szerrors.json)
1612        crate::error_mappings_generated::map_error_code(error_code, ctx)
1613    }
1614
1615    /// Gets the last exception message from the specified component
1616    fn get_last_exception_message(component: SzComponent, error_code: i64) -> String {
1617        use crate::ffi;
1618        use libc::c_char;
1619
1620        const BUFFER_SIZE: usize = 4096;
1621        let mut buffer = vec![0 as c_char; BUFFER_SIZE];
1622
1623        let result = unsafe {
1624            match component {
1625                SzComponent::Engine => {
1626                    ffi::Sz_getLastException(buffer.as_mut_ptr() as *mut c_char, BUFFER_SIZE)
1627                }
1628                SzComponent::Config => {
1629                    ffi::SzConfig_getLastException(buffer.as_mut_ptr() as *mut c_char, BUFFER_SIZE)
1630                }
1631                SzComponent::ConfigMgr => ffi::SzConfigMgr_getLastException(
1632                    buffer.as_mut_ptr() as *mut c_char,
1633                    BUFFER_SIZE,
1634                ),
1635                SzComponent::Diagnostic => ffi::SzDiagnostic_getLastException(
1636                    buffer.as_mut_ptr() as *mut c_char,
1637                    BUFFER_SIZE,
1638                ),
1639                SzComponent::Product => {
1640                    ffi::SzProduct_getLastException(buffer.as_mut_ptr() as *mut c_char, BUFFER_SIZE)
1641                }
1642            }
1643        };
1644
1645        if result > 0 {
1646            // Successfully got exception message
1647            unsafe {
1648                match CStr::from_ptr(buffer.as_ptr()).to_str() {
1649                    Ok(message) if !message.is_empty() => message.to_string(),
1650                    _ => format!("Native error (code: {error_code})"),
1651                }
1652            }
1653        } else {
1654            // Failed to get exception message, use generic message
1655            format!("Native error (code: {error_code})")
1656        }
1657    }
1658
1659    /// Creates an error from getLastExceptionCode() (legacy method for compatibility)
1660    pub fn from_code(error_code: i64) -> Self {
1661        // Default to Engine component for backward compatibility
1662        Self::from_code_with_message(error_code, SzComponent::Engine)
1663    }
1664
1665    /// Creates an Unknown error from a source error
1666    pub fn from_source(source: Box<dyn std::error::Error + Send + Sync>) -> Self {
1667        let message = source.to_string();
1668        Self::Unknown(ErrorContext {
1669            message,
1670            code: None,
1671            component: None,
1672            source: Some(source),
1673        })
1674    }
1675
1676    /// Creates an Unknown error with a custom message and source
1677    pub fn with_message_and_source<S: Into<String>>(
1678        message: S,
1679        source: Box<dyn std::error::Error + Send + Sync>,
1680    ) -> Self {
1681        Self::Unknown(ErrorContext {
1682            message: message.into(),
1683            code: None,
1684            component: None,
1685            source: Some(source),
1686        })
1687    }
1688}
1689
1690// ========================================================================
1691// ErrorContext Extension Methods
1692// ========================================================================
1693
1694impl ErrorContext {
1695    /// Adds a source error (builder pattern)
1696    ///
1697    /// This is useful for chaining error construction:
1698    ///
1699    /// ```no_run
1700    /// use sz_rust_sdk::error::ErrorContext;
1701    ///
1702    /// let ctx = ErrorContext::new("Parse failed")
1703    ///     .with_source(std::io::Error::other("IO error"));
1704    /// ```
1705    pub fn chain_source<E>(mut self, source: E) -> Self
1706    where
1707        E: std::error::Error + Send + Sync + 'static,
1708    {
1709        self.source = Some(Box::new(source));
1710        self
1711    }
1712}
1713
1714// ========================================================================
1715// SzError Extension Methods for Builder Pattern
1716// ========================================================================
1717
1718impl SzError {
1719    /// Adds a source error to this error (builder pattern)
1720    ///
1721    /// # Examples
1722    ///
1723    /// ```no_run
1724    /// use sz_rust_sdk::error::SzError;
1725    ///
1726    /// fn parse_config(data: &str) -> Result<(), SzError> {
1727    ///     let json_result: Result<serde_json::Value, _> = serde_json::from_str(data);
1728    ///     json_result.map_err(|e|
1729    ///         SzError::configuration("Invalid JSON config")
1730    ///             .with_source(e)
1731    ///     )?;
1732    ///     Ok(())
1733    /// }
1734    /// ```
1735    pub fn with_source<E>(mut self, source: E) -> Self
1736    where
1737        E: std::error::Error + Send + Sync + 'static,
1738    {
1739        match &mut self {
1740            Self::BadInput(ctx)
1741            | Self::Configuration(ctx)
1742            | Self::Database(ctx)
1743            | Self::License(ctx)
1744            | Self::NotFound(ctx)
1745            | Self::Retryable(ctx)
1746            | Self::Unrecoverable(ctx)
1747            | Self::Unknown(ctx)
1748            | Self::NotInitialized(ctx)
1749            | Self::DatabaseConnectionLost(ctx)
1750            | Self::DatabaseTransient(ctx)
1751            | Self::ReplaceConflict(ctx)
1752            | Self::RetryTimeoutExceeded(ctx)
1753            | Self::Unhandled(ctx)
1754            | Self::UnknownDataSource(ctx)
1755            | Self::EnvironmentDestroyed(ctx)
1756            | Self::Ffi(ctx) => {
1757                ctx.source = Some(Box::new(source));
1758            }
1759            // Json and StringConversion already have their source
1760            Self::Json(_) | Self::StringConversion(_) => {}
1761        }
1762        self
1763    }
1764}
1765
1766// ========================================================================
1767// Tests
1768// ========================================================================
1769
1770#[cfg(test)]
1771mod test_error_mapping {
1772    use super::*;
1773    use std::error::Error;
1774
1775    #[test]
1776    fn test_error_code_10_maps_to_retry_timeout() {
1777        let error = SzError::from_code(10);
1778        assert!(
1779            matches!(error, SzError::RetryTimeoutExceeded(_)),
1780            "Error code 10 should map to RetryTimeoutExceeded, got: {error:?}"
1781        );
1782        assert_eq!(error.error_code(), Some(10));
1783    }
1784
1785    #[test]
1786    fn test_error_code_87_maps_to_unhandled() {
1787        let error = SzError::from_code(87);
1788        assert!(
1789            matches!(error, SzError::Unhandled(_)),
1790            "Error code 87 should map to Unhandled, got: {error:?}"
1791        );
1792        assert_eq!(error.error_code(), Some(87));
1793    }
1794
1795    #[test]
1796    fn test_error_code_1006_maps_to_connection_lost() {
1797        let error = SzError::from_code(1006);
1798        assert!(
1799            matches!(error, SzError::DatabaseConnectionLost(_)),
1800            "Error code 1006 should map to DatabaseConnectionLost, got: {error:?}"
1801        );
1802        assert!(error.is_retryable());
1803        assert_eq!(error.error_code(), Some(1006));
1804    }
1805
1806    #[test]
1807    fn test_error_code_1007_maps_to_connection_lost() {
1808        let error = SzError::from_code(1007);
1809        assert!(
1810            matches!(error, SzError::DatabaseConnectionLost(_)),
1811            "Error code 1007 should map to DatabaseConnectionLost, got: {error:?}"
1812        );
1813        assert!(error.is_retryable());
1814    }
1815
1816    #[test]
1817    fn test_error_code_1008_maps_to_database_transient() {
1818        let error = SzError::from_code(1008);
1819        assert!(
1820            matches!(error, SzError::DatabaseTransient(_)),
1821            "Error code 1008 should map to DatabaseTransient, got: {error:?}"
1822        );
1823        assert!(error.is_retryable());
1824        assert_eq!(error.error_code(), Some(1008));
1825    }
1826
1827    #[test]
1828    fn test_not_initialized_error_codes() {
1829        for code in [48, 49, 50, 53] {
1830            let error = SzError::from_code(code);
1831            assert!(
1832                matches!(error, SzError::NotInitialized(_)),
1833                "Error code {code} should map to NotInitialized, got: {error:?}"
1834            );
1835        }
1836    }
1837
1838    #[test]
1839    fn test_license_error_code_999() {
1840        let error = SzError::from_code(999);
1841        assert!(
1842            matches!(error, SzError::License(_)),
1843            "Error code 999 should map to License, got: {error:?}"
1844        );
1845        assert_eq!(error.error_code(), Some(999));
1846    }
1847
1848    #[test]
1849    fn test_database_error_range() {
1850        let error = SzError::from_code(1010);
1851        assert!(
1852            matches!(error, SzError::Database(_)),
1853            "Error code 1010 should map to Database, got: {error:?}"
1854        );
1855    }
1856
1857    #[test]
1858    fn test_bad_input_range() {
1859        // Test codes that map to BadInput (excluding NotFound/UnknownDataSource subtypes)
1860        for code in [2, 7, 22, 51, 88] {
1861            let error = SzError::from_code(code);
1862            assert!(
1863                matches!(error, SzError::BadInput(_)),
1864                "Error code {code} should map to BadInput, got: {error:?}"
1865            );
1866        }
1867
1868        // Code 33 is NotFound (subtype of BadInput)
1869        let error = SzError::from_code(33);
1870        assert!(
1871            matches!(error, SzError::NotFound(_)),
1872            "Error code 33 should map to NotFound, got: {error:?}"
1873        );
1874        // But it should still be recognized as BadInput category
1875        assert!(error.is_bad_input());
1876    }
1877
1878    #[test]
1879    fn test_configuration_range() {
1880        let error = SzError::from_code(2001);
1881        assert!(
1882            matches!(error, SzError::Configuration(_)),
1883            "Error code 2001 should map to Configuration, got: {error:?}"
1884        );
1885    }
1886
1887    #[test]
1888    fn test_unknown_error_default() {
1889        let error = SzError::from_code(99999);
1890        assert!(
1891            matches!(error, SzError::Unknown(_)),
1892            "Error code 99999 should map to Unknown, got: {error:?}"
1893        );
1894    }
1895
1896    #[test]
1897    fn test_from_code_with_message() {
1898        let error = SzError::from_code_with_message(999, SzComponent::Config);
1899        assert!(matches!(error, SzError::License(_)));
1900        assert_eq!(error.component(), Some(SzComponent::Config));
1901        assert_eq!(error.error_code(), Some(999));
1902    }
1903
1904    #[test]
1905    fn test_error_with_source() {
1906        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
1907        let error = SzError::configuration("Parse failed").with_source(json_err);
1908
1909        assert!(matches!(error, SzError::Configuration(_)));
1910        assert!(error.source().is_some());
1911    }
1912
1913    #[test]
1914    fn test_is_retryable_methods() {
1915        assert!(SzError::retry_timeout_exceeded("Timeout").is_retryable());
1916        assert!(SzError::database_connection_lost("Lost").is_retryable());
1917        assert!(SzError::database_transient("Deadlock").is_retryable());
1918        assert!(!SzError::bad_input("Invalid").is_retryable());
1919    }
1920
1921    #[test]
1922    fn test_is_unrecoverable_methods() {
1923        assert!(SzError::license("Expired").is_unrecoverable());
1924        assert!(SzError::not_initialized("Not init").is_unrecoverable());
1925        assert!(SzError::database("DB error").is_unrecoverable());
1926        assert!(!SzError::bad_input("Invalid").is_unrecoverable());
1927    }
1928
1929    #[test]
1930    fn test_is_bad_input_methods() {
1931        assert!(SzError::bad_input("Invalid").is_bad_input());
1932        assert!(SzError::not_found("Missing").is_bad_input());
1933        assert!(SzError::unknown_data_source("Unknown").is_bad_input());
1934        assert!(!SzError::configuration("Config").is_bad_input());
1935    }
1936
1937    #[test]
1938    fn test_error_context_preservation() {
1939        let error = SzError::from_code_with_message(1008, SzComponent::Engine);
1940
1941        assert_eq!(error.error_code(), Some(1008));
1942        assert_eq!(error.component(), Some(SzComponent::Engine));
1943        assert!(error.is_retryable());
1944    }
1945
1946    #[test]
1947    fn test_error_category() {
1948        assert_eq!(
1949            SzError::database_transient("test").category(),
1950            "database_transient"
1951        );
1952        assert_eq!(SzError::license("test").category(), "license");
1953        assert_eq!(SzError::bad_input("test").category(), "bad_input");
1954        assert_eq!(SzError::not_found("test").category(), "bad_input");
1955        assert_eq!(SzError::configuration("test").category(), "configuration");
1956    }
1957
1958    #[test]
1959    fn test_error_severity() {
1960        assert_eq!(SzError::license("test").severity(), "critical");
1961        assert_eq!(SzError::unhandled("test").severity(), "critical");
1962        assert_eq!(SzError::database("test").severity(), "high");
1963        assert_eq!(SzError::database_transient("test").severity(), "medium");
1964        assert_eq!(SzError::bad_input("test").severity(), "low");
1965    }
1966
1967    #[test]
1968    fn test_error_metadata_complete() {
1969        let error = SzError::from_code_with_message(1008, SzComponent::Engine);
1970
1971        // Verify all metadata is accessible
1972        assert_eq!(error.error_code(), Some(1008));
1973        assert_eq!(error.component(), Some(SzComponent::Engine));
1974        assert_eq!(error.category(), "database_transient");
1975        assert_eq!(error.severity(), "medium");
1976        assert!(error.is_retryable());
1977        assert!(!error.is_unrecoverable());
1978        assert!(!error.is_bad_input());
1979    }
1980
1981    #[test]
1982    fn test_result_ext_or_retry() {
1983        use super::SzResultExt;
1984
1985        // Retryable error should trigger retry
1986        let result: SzResult<i32> = Err(SzError::database_transient("Deadlock"));
1987        let retried = result.or_retry(|e| {
1988            assert!(e.is_retryable());
1989            Ok(42)
1990        });
1991        assert_eq!(retried.unwrap(), 42);
1992
1993        // Non-retryable error should propagate
1994        let result: SzResult<i32> = Err(SzError::license("Expired"));
1995        let retried = result.or_retry(|_| Ok(42));
1996        assert!(retried.is_err());
1997        assert!(retried.unwrap_err().is_unrecoverable());
1998    }
1999
2000    #[test]
2001    fn test_result_ext_filter_retryable() {
2002        use super::SzResultExt;
2003
2004        // Success should return Some
2005        let result: SzResult<i32> = Ok(42);
2006        assert_eq!(result.filter_retryable().unwrap(), Some(42));
2007
2008        // Retryable error should return None
2009        let result: SzResult<i32> = Err(SzError::database_transient("Deadlock"));
2010        assert_eq!(result.filter_retryable().unwrap(), None);
2011
2012        // Non-retryable error should propagate
2013        let result: SzResult<i32> = Err(SzError::license("Expired"));
2014        assert!(result.filter_retryable().is_err());
2015    }
2016
2017    #[test]
2018    fn test_result_ext_is_retryable_error() {
2019        use super::SzResultExt;
2020
2021        let ok_result: SzResult<i32> = Ok(42);
2022        assert!(!ok_result.is_retryable_error());
2023
2024        let retryable: SzResult<i32> = Err(SzError::database_transient("Deadlock"));
2025        assert!(retryable.is_retryable_error());
2026
2027        let not_retryable: SzResult<i32> = Err(SzError::license("Expired"));
2028        assert!(!not_retryable.is_retryable_error());
2029    }
2030
2031    #[test]
2032    fn test_result_ext_is_unrecoverable_error() {
2033        use super::SzResultExt;
2034
2035        let ok_result: SzResult<i32> = Ok(42);
2036        assert!(!ok_result.is_unrecoverable_error());
2037
2038        let unrecoverable: SzResult<i32> = Err(SzError::license("Expired"));
2039        assert!(unrecoverable.is_unrecoverable_error());
2040
2041        let recoverable: SzResult<i32> = Err(SzError::bad_input("Invalid"));
2042        assert!(!recoverable.is_unrecoverable_error());
2043    }
2044
2045    #[test]
2046    fn test_result_ext_is_bad_input_error() {
2047        use super::SzResultExt;
2048
2049        let ok_result: SzResult<i32> = Ok(42);
2050        assert!(!ok_result.is_bad_input_error());
2051
2052        let bad_input: SzResult<i32> = Err(SzError::bad_input("Invalid"));
2053        assert!(bad_input.is_bad_input_error());
2054
2055        let not_bad_input: SzResult<i32> = Err(SzError::license("Expired"));
2056        assert!(!not_bad_input.is_bad_input_error());
2057    }
2058
2059    #[test]
2060    fn test_is_database_methods() {
2061        // All database-related errors
2062        assert!(SzError::database("Schema error").is_database());
2063        assert!(SzError::database_connection_lost("Lost").is_database());
2064        assert!(SzError::database_transient("Deadlock").is_database());
2065
2066        // Non-database errors
2067        assert!(!SzError::license("Expired").is_database());
2068        assert!(!SzError::configuration("Invalid").is_database());
2069
2070        // Database errors can be retryable or unrecoverable
2071        assert!(SzError::database("Schema").is_database());
2072        assert!(SzError::database("Schema").is_unrecoverable());
2073
2074        assert!(SzError::database_transient("Deadlock").is_database());
2075        assert!(SzError::database_transient("Deadlock").is_retryable());
2076    }
2077
2078    #[test]
2079    fn test_is_license_methods() {
2080        assert!(SzError::license("Expired").is_license());
2081        assert!(!SzError::database("Error").is_license());
2082    }
2083
2084    #[test]
2085    fn test_is_configuration_methods() {
2086        assert!(SzError::configuration("Invalid").is_configuration());
2087        assert!(!SzError::database("Error").is_configuration());
2088    }
2089
2090    #[test]
2091    fn test_is_initialization_methods() {
2092        assert!(SzError::not_initialized("Not init").is_initialization());
2093        assert!(!SzError::database("Error").is_initialization());
2094    }
2095
2096    #[test]
2097    fn test_error_domain_and_behavior_combined() {
2098        // Database error that's retryable
2099        let error = SzError::database_transient("Deadlock");
2100        assert!(error.is_database());
2101        assert!(error.is_retryable());
2102        assert!(!error.is_unrecoverable());
2103
2104        // Database error that's unrecoverable
2105        let error = SzError::database("Schema error");
2106        assert!(error.is_database());
2107        assert!(error.is_unrecoverable());
2108        assert!(!error.is_retryable());
2109    }
2110
2111    // ========================================================================
2112    // Error Hierarchy Tests
2113    // ========================================================================
2114
2115    #[test]
2116    fn test_hierarchy_database_transient() {
2117        let err = SzError::database_transient("Deadlock");
2118        let hierarchy = err.hierarchy();
2119
2120        assert_eq!(hierarchy.len(), 2);
2121        assert_eq!(hierarchy[0], ErrorCategory::DatabaseTransient);
2122        assert_eq!(hierarchy[1], ErrorCategory::Retryable);
2123    }
2124
2125    #[test]
2126    fn test_hierarchy_not_found() {
2127        let err = SzError::not_found("Entity 123");
2128        let hierarchy = err.hierarchy();
2129
2130        assert_eq!(hierarchy.len(), 2);
2131        assert_eq!(hierarchy[0], ErrorCategory::NotFound);
2132        assert_eq!(hierarchy[1], ErrorCategory::BadInput);
2133    }
2134
2135    #[test]
2136    fn test_hierarchy_database() {
2137        let err = SzError::database("Schema error");
2138        let hierarchy = err.hierarchy();
2139
2140        assert_eq!(hierarchy.len(), 2);
2141        assert_eq!(hierarchy[0], ErrorCategory::Database);
2142        assert_eq!(hierarchy[1], ErrorCategory::Unrecoverable);
2143    }
2144
2145    #[test]
2146    fn test_hierarchy_license() {
2147        let err = SzError::license("License expired");
2148        let hierarchy = err.hierarchy();
2149
2150        assert_eq!(hierarchy.len(), 2);
2151        assert_eq!(hierarchy[0], ErrorCategory::License);
2152        assert_eq!(hierarchy[1], ErrorCategory::Unrecoverable);
2153    }
2154
2155    #[test]
2156    fn test_hierarchy_configuration() {
2157        let err = SzError::configuration("Invalid config");
2158        let hierarchy = err.hierarchy();
2159
2160        assert_eq!(hierarchy.len(), 1);
2161        assert_eq!(hierarchy[0], ErrorCategory::Configuration);
2162    }
2163
2164    #[test]
2165    fn test_is_method_specific_type() {
2166        let err = SzError::database_transient("Deadlock");
2167
2168        // Should match specific type
2169        assert!(err.is(ErrorCategory::DatabaseTransient));
2170    }
2171
2172    #[test]
2173    fn test_is_method_parent_type() {
2174        let err = SzError::database_transient("Deadlock");
2175
2176        // Should match parent type (polymorphic)
2177        assert!(err.is(ErrorCategory::Retryable));
2178    }
2179
2180    #[test]
2181    fn test_is_method_negative() {
2182        let err = SzError::database_transient("Deadlock");
2183
2184        // Should NOT match unrelated types
2185        assert!(!err.is(ErrorCategory::BadInput));
2186        assert!(!err.is(ErrorCategory::Unrecoverable));
2187        assert!(!err.is(ErrorCategory::Configuration));
2188    }
2189
2190    #[test]
2191    fn test_is_method_all_retryable_subtypes() {
2192        // All Retryable subtypes should match Retryable category
2193        assert!(SzError::database_connection_lost("Lost").is(ErrorCategory::Retryable));
2194        assert!(SzError::database_transient("Deadlock").is(ErrorCategory::Retryable));
2195        assert!(SzError::retry_timeout_exceeded("Timeout").is(ErrorCategory::Retryable));
2196    }
2197
2198    #[test]
2199    fn test_is_method_all_unrecoverable_subtypes() {
2200        // All Unrecoverable subtypes should match Unrecoverable category
2201        assert!(SzError::database("DB error").is(ErrorCategory::Unrecoverable));
2202        assert!(SzError::license("Expired").is(ErrorCategory::Unrecoverable));
2203        assert!(SzError::not_initialized("Not init").is(ErrorCategory::Unrecoverable));
2204        assert!(SzError::unhandled("Unhandled").is(ErrorCategory::Unrecoverable));
2205    }
2206
2207    #[test]
2208    fn test_is_method_all_bad_input_subtypes() {
2209        // All BadInput subtypes should match BadInput category
2210        assert!(SzError::not_found("Missing").is(ErrorCategory::BadInput));
2211        assert!(SzError::unknown_data_source("Unknown").is(ErrorCategory::BadInput));
2212        assert!(SzError::bad_input("Invalid").is(ErrorCategory::BadInput));
2213    }
2214
2215    // ========================================================================
2216    // Generated Error Code Mapping Tests
2217    // ========================================================================
2218
2219    #[test]
2220    fn test_generated_mapping_sample_codes() {
2221        // Test a sample of error codes from different ranges to verify generated mappings
2222
2223        // BadInput range
2224        let err = SzError::from_code(2);
2225        assert!(matches!(err, SzError::BadInput(_)));
2226
2227        let err = SzError::from_code(7);
2228        assert!(matches!(err, SzError::BadInput(_)));
2229
2230        // RetryTimeoutExceeded (specific code 10)
2231        let err = SzError::from_code(10);
2232        assert!(matches!(err, SzError::RetryTimeoutExceeded(_)));
2233
2234        // Configuration range
2235        let err = SzError::from_code(14);
2236        assert!(matches!(err, SzError::Configuration(_)));
2237
2238        // NotInitialized (specific codes)
2239        let err = SzError::from_code(48);
2240        assert!(matches!(err, SzError::NotInitialized(_)));
2241
2242        // Unhandled (specific code 87)
2243        let err = SzError::from_code(87);
2244        assert!(matches!(err, SzError::Unhandled(_)));
2245
2246        // License (specific code 999)
2247        let err = SzError::from_code(999);
2248        assert!(matches!(err, SzError::License(_)));
2249
2250        // DatabaseConnectionLost (specific codes)
2251        let err = SzError::from_code(1006);
2252        assert!(matches!(err, SzError::DatabaseConnectionLost(_)));
2253
2254        let err = SzError::from_code(1007);
2255        assert!(matches!(err, SzError::DatabaseConnectionLost(_)));
2256
2257        // DatabaseTransient (specific code 1008)
2258        let err = SzError::from_code(1008);
2259        assert!(matches!(err, SzError::DatabaseTransient(_)));
2260
2261        // Database (other codes in range)
2262        let err = SzError::from_code(1010);
2263        assert!(matches!(err, SzError::Database(_)));
2264
2265        // Configuration (2000-2300 range)
2266        let err = SzError::from_code(2001);
2267        assert!(matches!(err, SzError::Configuration(_)));
2268    }
2269
2270    #[test]
2271    fn test_generated_mapping_with_hierarchy() {
2272        // Test that generated mappings provide correct hierarchy
2273        let err = SzError::from_code(1008); // DatabaseTransient
2274
2275        // Verify error code is preserved
2276        assert_eq!(err.error_code(), Some(1008));
2277
2278        // Verify hierarchy is correct
2279        assert!(err.is(ErrorCategory::DatabaseTransient));
2280        assert!(err.is(ErrorCategory::Retryable));
2281
2282        // Verify it's recognized as retryable
2283        assert!(err.is_retryable());
2284    }
2285
2286    #[test]
2287    fn test_all_specific_error_codes() {
2288        // Test all the specific error codes that have special handling
2289        let test_cases = vec![
2290            (
2291                10,
2292                ErrorCategory::RetryTimeoutExceeded,
2293                ErrorCategory::Retryable,
2294            ),
2295            (87, ErrorCategory::Unhandled, ErrorCategory::Unrecoverable),
2296            (
2297                48,
2298                ErrorCategory::NotInitialized,
2299                ErrorCategory::Unrecoverable,
2300            ),
2301            (
2302                49,
2303                ErrorCategory::NotInitialized,
2304                ErrorCategory::Unrecoverable,
2305            ),
2306            (
2307                50,
2308                ErrorCategory::NotInitialized,
2309                ErrorCategory::Unrecoverable,
2310            ),
2311            (
2312                53,
2313                ErrorCategory::NotInitialized,
2314                ErrorCategory::Unrecoverable,
2315            ),
2316            (999, ErrorCategory::License, ErrorCategory::Unrecoverable),
2317            (
2318                1006,
2319                ErrorCategory::DatabaseConnectionLost,
2320                ErrorCategory::Retryable,
2321            ),
2322            (
2323                1007,
2324                ErrorCategory::DatabaseConnectionLost,
2325                ErrorCategory::Retryable,
2326            ),
2327            (
2328                1008,
2329                ErrorCategory::DatabaseTransient,
2330                ErrorCategory::Retryable,
2331            ),
2332        ];
2333
2334        for (code, specific, parent) in test_cases {
2335            let err = SzError::from_code(code);
2336            assert!(
2337                err.is(specific),
2338                "Error code {} should be {:?}",
2339                code,
2340                specific
2341            );
2342            assert!(
2343                err.is(parent),
2344                "Error code {} should also be {:?} (parent)",
2345                code,
2346                parent
2347            );
2348        }
2349    }
2350
2351    #[test]
2352    fn test_error_code_preservation() {
2353        // Verify that error codes are preserved through the mapping process
2354        for code in [2, 10, 48, 87, 999, 1006, 1008, 2001] {
2355            let err = SzError::from_code(code);
2356            assert_eq!(
2357                err.error_code(),
2358                Some(code),
2359                "Error code {} should be preserved",
2360                code
2361            );
2362        }
2363    }
2364}
2365
2366#[cfg(test)]
2367mod test_sz_error_inspect {
2368    use super::*;
2369
2370    // A custom wrapper error to test chain walking
2371    #[derive(Debug)]
2372    struct AppError {
2373        source: Box<dyn std::error::Error + Send + Sync>,
2374    }
2375
2376    impl std::fmt::Display for AppError {
2377        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2378            write!(f, "app error")
2379        }
2380    }
2381
2382    impl std::error::Error for AppError {
2383        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2384            Some(&*self.source)
2385        }
2386    }
2387
2388    #[test]
2389    fn test_find_in_chain_direct_sz_error() {
2390        let err = SzError::database_transient("Deadlock");
2391        let found = SzError::find_in_chain(&err);
2392        assert!(found.is_some());
2393        assert!(found.unwrap().is_retryable());
2394    }
2395
2396    #[test]
2397    fn test_find_in_chain_no_sz_error() {
2398        let err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
2399        assert!(SzError::find_in_chain(&err).is_none());
2400    }
2401
2402    #[test]
2403    fn test_find_in_chain_wrapped_sz_error() {
2404        let sz = SzError::license("Expired");
2405        let wrapper = AppError {
2406            source: Box::new(sz),
2407        };
2408        let found = SzError::find_in_chain(&wrapper);
2409        assert!(found.is_some());
2410        assert!(found.unwrap().is_license());
2411    }
2412
2413    #[test]
2414    fn test_find_in_chain_double_wrapped() {
2415        let sz = SzError::not_found("Entity 42");
2416        let inner = AppError {
2417            source: Box::new(sz),
2418        };
2419        let outer = AppError {
2420            source: Box::new(inner),
2421        };
2422        let found = SzError::find_in_chain(&outer);
2423        assert!(found.is_some());
2424        assert!(found.unwrap().is_bad_input());
2425    }
2426
2427    #[test]
2428    fn test_inspect_trait_on_sz_error() {
2429        let err = SzError::database_connection_lost("Connection reset");
2430        assert!(err.is_sz_retryable());
2431        assert!(!err.is_sz_unrecoverable());
2432        assert!(!err.is_sz_bad_input());
2433        assert!(err.is_sz(ErrorCategory::Retryable));
2434        assert!(err.is_sz(ErrorCategory::DatabaseConnectionLost));
2435        assert!(!err.is_sz(ErrorCategory::BadInput));
2436    }
2437
2438    #[test]
2439    fn test_inspect_trait_on_non_sz_error() {
2440        let err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broke");
2441        assert!(!err.is_sz_retryable());
2442        assert!(!err.is_sz_unrecoverable());
2443        assert!(!err.is_sz_bad_input());
2444        assert!(!err.is_sz(ErrorCategory::Retryable));
2445        assert!(err.sz_error().is_none());
2446    }
2447
2448    #[test]
2449    fn test_inspect_trait_on_wrapped_error() {
2450        let sz = SzError::database_transient("Deadlock");
2451        let wrapper = AppError {
2452            source: Box::new(sz),
2453        };
2454        assert!(wrapper.is_sz_retryable());
2455        assert!(!wrapper.is_sz_unrecoverable());
2456        assert!(wrapper.is_sz(ErrorCategory::DatabaseTransient));
2457        assert!(wrapper.is_sz(ErrorCategory::Retryable));
2458    }
2459
2460    #[test]
2461    fn test_inspect_trait_on_box_dyn_error() {
2462        let sz = SzError::not_initialized("SDK not initialized");
2463        let boxed: Box<dyn std::error::Error> = Box::new(sz);
2464        assert!(boxed.is_sz_unrecoverable());
2465        assert!(!boxed.is_sz_retryable());
2466        assert!(boxed.is_sz(ErrorCategory::NotInitialized));
2467        assert!(boxed.is_sz(ErrorCategory::Unrecoverable));
2468    }
2469
2470    #[test]
2471    fn test_inspect_trait_on_box_dyn_error_send_sync() {
2472        let sz = SzError::unknown_data_source("FAKE_SOURCE");
2473        let boxed: Box<dyn std::error::Error + Send + Sync> = Box::new(sz);
2474        assert!(boxed.is_sz_bad_input());
2475        assert!(boxed.is_sz(ErrorCategory::UnknownDataSource));
2476        assert!(boxed.is_sz(ErrorCategory::BadInput));
2477    }
2478
2479    #[test]
2480    fn test_inspect_sz_error_returns_reference() {
2481        let sz = SzError::configuration("Bad config");
2482        let wrapper = AppError {
2483            source: Box::new(sz),
2484        };
2485        let found = wrapper.sz_error().unwrap();
2486        assert!(found.is_configuration());
2487        assert_eq!(found.message(), "Bad config");
2488    }
2489
2490    #[test]
2491    fn test_inspect_all_categories() {
2492        // Verify every category is reachable through the trait
2493        let cases: Vec<(SzError, ErrorCategory)> = vec![
2494            (SzError::bad_input("x"), ErrorCategory::BadInput),
2495            (SzError::not_found("x"), ErrorCategory::NotFound),
2496            (
2497                SzError::unknown_data_source("x"),
2498                ErrorCategory::UnknownDataSource,
2499            ),
2500            (SzError::retryable("x"), ErrorCategory::Retryable),
2501            (
2502                SzError::database_connection_lost("x"),
2503                ErrorCategory::DatabaseConnectionLost,
2504            ),
2505            (
2506                SzError::database_transient("x"),
2507                ErrorCategory::DatabaseTransient,
2508            ),
2509            (
2510                SzError::retry_timeout_exceeded("x"),
2511                ErrorCategory::RetryTimeoutExceeded,
2512            ),
2513            (SzError::unrecoverable("x"), ErrorCategory::Unrecoverable),
2514            (SzError::database("x"), ErrorCategory::Database),
2515            (SzError::license("x"), ErrorCategory::License),
2516            (SzError::not_initialized("x"), ErrorCategory::NotInitialized),
2517            (SzError::unhandled("x"), ErrorCategory::Unhandled),
2518            (SzError::configuration("x"), ErrorCategory::Configuration),
2519            (
2520                SzError::replace_conflict("x"),
2521                ErrorCategory::ReplaceConflict,
2522            ),
2523            (
2524                SzError::environment_destroyed("x"),
2525                ErrorCategory::EnvironmentDestroyed,
2526            ),
2527            (SzError::unknown("x"), ErrorCategory::Unknown),
2528        ];
2529
2530        for (err, expected_cat) in cases {
2531            let wrapper = AppError {
2532                source: Box::new(err),
2533            };
2534            assert!(
2535                wrapper.is_sz(expected_cat),
2536                "Wrapped error should match {:?}",
2537                expected_cat
2538            );
2539        }
2540    }
2541}