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}