Skip to main content

copybook_error/
lib.rs

1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Error types and taxonomy for copybook-rs
4//!
5//! This module defines a comprehensive error taxonomy with stable error codes
6//! for all failure modes in the copybook processing system.
7
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use thiserror::Error;
11
12/// Result type alias for copybook operations
13pub type Result<T> = std::result::Result<T, Error>;
14
15/// Main error type for copybook operations
16///
17/// Uses thiserror for clean error handling with manual Display implementation
18/// to avoid allocations in hot paths.
19///
20/// # Examples
21///
22/// ```
23/// use copybook_error::{Error, ErrorCode};
24///
25/// let err = Error::new(ErrorCode::CBKP001_SYNTAX, "unexpected token");
26/// assert_eq!(err.code(), ErrorCode::CBKP001_SYNTAX);
27/// assert_eq!(err.family_prefix(), "CBKP");
28/// assert_eq!(err.to_string(), "CBKP001_SYNTAX: unexpected token");
29/// ```
30#[derive(Error, Debug, Clone, PartialEq)]
31pub struct Error {
32    /// Stable error code for programmatic handling
33    pub code: ErrorCode,
34
35    /// Human-readable error message
36    pub message: String,
37
38    /// Optional context information
39    pub context: Option<ErrorContext>,
40}
41
42// Manual Display implementation to avoid allocations when context is None
43impl fmt::Display for Error {
44    #[inline]
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "{}: {}", self.code, self.message)?;
47        if let Some(ref ctx) = self.context {
48            write!(f, " ({ctx})")?;
49        }
50        Ok(())
51    }
52}
53
54impl Error {
55    /// Return the stable error code for this error.
56    #[inline]
57    #[must_use]
58    pub const fn code(&self) -> ErrorCode {
59        self.code
60    }
61
62    /// Return the CBK* family prefix associated with this error.
63    #[inline]
64    #[must_use]
65    pub const fn family_prefix(&self) -> &'static str {
66        self.code.family_prefix()
67    }
68}
69
70/// Stable error codes for programmatic error handling
71///
72/// The copybook-rs error taxonomy uses a structured approach with stable error codes
73/// that enable programmatic error handling across all components. Each code follows
74/// the pattern `CBK[Category][Number]_[Description]` where:
75///
76/// - **CBKP**: Parse errors during copybook analysis
77/// - **CBKS**: Schema validation and ODO processing
78/// - **CBKR**: Record format and RDW processing
79/// - **CBKC**: Character conversion and encoding
80/// - **CBKD**: Data decoding and field validation
81/// - **CBKE**: Encoding and JSON serialization
82/// - **CBKF**: File format and structure validation
83/// - **CBKI**: Iterator and infrastructure state validation (e.g., fixed-format without LRECL -> `CBKI001_INVALID_STATE`)
84/// - **CBKA**: Audit operations (performance baselines)
85/// - **CBKW**: Arrow/Writer errors (Apache Arrow and Parquet conversion)
86///
87/// Implements `Serialize`/`Deserialize` for error code persistence and API responses.
88///
89/// # Examples
90///
91/// ```
92/// use copybook_error::ErrorCode;
93///
94/// let code = ErrorCode::CBKP001_SYNTAX;
95/// assert_eq!(code.family_prefix(), "CBKP");
96/// assert_eq!(format!("{code}"), "CBKP001_SYNTAX");
97///
98/// let data_code = ErrorCode::CBKD301_RECORD_TOO_SHORT;
99/// assert_eq!(data_code.family_prefix(), "CBKD");
100/// ```
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
102#[allow(non_camel_case_types)] // These are stable external error codes
103pub enum ErrorCode {
104    // =============================================================================
105    // Parse Errors (CBKP*) - Copybook syntax and COBOL clause processing
106    // =============================================================================
107    /// CBKP001: General copybook syntax error during parsing
108    CBKP001_SYNTAX,
109    /// CBKP011: Unsupported COBOL clause or feature encountered
110    CBKP011_UNSUPPORTED_CLAUSE,
111    /// CBKP021: ODO (OCCURS DEPENDING ON) array not at tail position
112    CBKP021_ODO_NOT_TAIL,
113    /// CBKP022: Nested ODO (OCCURS DEPENDING ON inside another OCCURS/ODO)
114    CBKP022_NESTED_ODO,
115    /// CBKP023: ODO (OCCURS DEPENDING ON) over REDEFINES
116    CBKP023_ODO_REDEFINES,
117    /// CBKP051: Unsupported edited PIC clause pattern
118    CBKP051_UNSUPPORTED_EDITED_PIC,
119    /// CBKP101: Invalid PIC clause syntax or illegal characters
120    CBKP101_INVALID_PIC,
121
122    // =============================================================================
123    // Schema Errors (CBKS*) - Schema validation and ODO processing
124    // =============================================================================
125    /// CBKS121: ODO counter field not found in schema
126    CBKS121_COUNTER_NOT_FOUND,
127    /// CBKS141: Record size exceeds maximum allowable limit
128    CBKS141_RECORD_TOO_LARGE,
129    /// CBKS301: ODO count clipped to maximum allowed value (warning)
130    CBKS301_ODO_CLIPPED,
131    /// CBKS302: ODO count raised to minimum required value (warning)
132    CBKS302_ODO_RAISED,
133    /// CBKS601: RENAMES from field not found in scope
134    CBKS601_RENAME_UNKNOWN_FROM,
135    /// CBKS602: RENAMES thru field not found in scope
136    CBKS602_RENAME_UNKNOWN_THRU,
137    /// CBKS603: RENAMES range is not contiguous (gap between from and thru)
138    CBKS603_RENAME_NOT_CONTIGUOUS,
139    /// CBKS604: RENAMES range is reversed (from comes after thru)
140    CBKS604_RENAME_REVERSED_RANGE,
141    /// CBKS605: RENAMES from field crosses group boundary
142    CBKS605_RENAME_FROM_CROSSES_GROUP,
143    /// CBKS606: RENAMES thru field crosses group boundary
144    CBKS606_RENAME_THRU_CROSSES_GROUP,
145    /// CBKS607: RENAMES range crosses OCCURS boundary
146    CBKS607_RENAME_CROSSES_OCCURS,
147    /// CBKS608: RENAMES qualified name not found
148    CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND,
149    /// CBKS609: RENAMES alias spans REDEFINES field(s) (R4 scenario)
150    CBKS609_RENAME_OVER_REDEFINES,
151    /// CBKS610: RENAMES spans multiple REDEFINES alternatives (R4 scenario)
152    CBKS610_RENAME_MULTIPLE_REDEFINES,
153    /// CBKS611: RENAMES spans partial array elements (R5 scenario)
154    CBKS611_RENAME_PARTIAL_OCCURS,
155    /// CBKS612: RENAMES with ODO arrays not supported (R5 scenario)
156    CBKS612_RENAME_ODO_NOT_SUPPORTED,
157    /// CBKS701: Field projection error - ODO array without accessible counter
158    CBKS701_PROJECTION_INVALID_ODO,
159    /// CBKS702: Field projection error - RENAMES alias spans unselected fields
160    CBKS702_PROJECTION_UNRESOLVED_ALIAS,
161    /// CBKS703: Field projection error - selected field not found in schema
162    CBKS703_PROJECTION_FIELD_NOT_FOUND,
163
164    // =============================================================================
165    // Record Errors (CBKR*) - Record format and RDW processing
166    // =============================================================================
167    /// CBKR101: Fixed-length record framing error
168    CBKR101_FIXED_RECORD_ERROR,
169    /// CBKR201: Error reading an RDW (Record Descriptor Word) header or payload
170    CBKR201_RDW_READ_ERROR,
171    /// CBKR202: Error writing or flushing an RDW (Record Descriptor Word)
172    /// header or payload
173    CBKR202_RDW_WRITE_ERROR,
174    /// CBKR211: RDW reserved bytes contain non-zero values
175    CBKR211_RDW_RESERVED_NONZERO,
176
177    // =============================================================================
178    // Character Conversion Errors (CBKC*) - EBCDIC/ASCII conversion
179    // =============================================================================
180    /// CBKC201: JSON serialization write error
181    CBKC201_JSON_WRITE_ERROR,
182    /// CBKC301: Invalid EBCDIC byte encountered during conversion
183    CBKC301_INVALID_EBCDIC_BYTE,
184
185    // =============================================================================
186    // Data Decode Errors (CBKD*) - Field validation and numeric processing
187    // =============================================================================
188    /// CBKD101: Invalid field type for requested operation
189    CBKD101_INVALID_FIELD_TYPE,
190    /// CBKD301: Record data too short for field requirements
191    CBKD301_RECORD_TOO_SHORT,
192    /// CBKD302: Legacy edited PIC identifier retained for compatibility; not emitted by current paths.
193    CBKD302_EDITED_PIC_NOT_IMPLEMENTED,
194    /// CBKD401: Invalid packed decimal nibble value
195    CBKD401_COMP3_INVALID_NIBBLE,
196    /// CBKD410: Zoned decimal value exceeded numeric capacity
197    CBKD410_ZONED_OVERFLOW,
198    /// CBKD411: Invalid zoned decimal sign zone
199    CBKD411_ZONED_BAD_SIGN,
200    /// CBKD412: Zoned field contains all spaces (BLANK WHEN ZERO processing)
201    CBKD412_ZONED_BLANK_IS_ZERO,
202    /// CBKD413: Invalid zoned decimal encoding format detected
203    CBKD413_ZONED_INVALID_ENCODING,
204    /// CBKD414: Mixed ASCII/EBCDIC encoding within single zoned field
205    CBKD414_ZONED_MIXED_ENCODING,
206    /// CBKD415: Zoned encoding detection failed or remains ambiguous
207    CBKD415_ZONED_ENCODING_AMBIGUOUS,
208    /// CBKD421: Edited PIC decode failed - invalid format (mismatch between data and pattern)
209    CBKD421_EDITED_PIC_INVALID_FORMAT,
210    /// CBKD422: Edited PIC sign editing mismatch
211    CBKD422_EDITED_PIC_SIGN_MISMATCH,
212    /// CBKD423: Edited PIC blank when zero handling error
213    CBKD423_EDITED_PIC_BLANK_WHEN_ZERO,
214    /// CBKD431: Floating-point field contains NaN. Reserved — not currently
215    /// emitted; the decode path converts NaN to JSON `null` instead of raising
216    /// this code. See `docs/reference/ERROR_CODES.md`.
217    CBKD431_FLOAT_NAN,
218    /// CBKD432: Floating-point field contains infinity. Reserved — not
219    /// currently emitted; the decode path converts ±Infinity to JSON `null`
220    /// instead of raising this code. See `docs/reference/ERROR_CODES.md`.
221    CBKD432_FLOAT_INFINITY,
222
223    // =============================================================================
224    // Infrastructure Errors (CBKI*) - Iterator and internal state validation
225    // =============================================================================
226    /// CBKI001: Iterator or decoder encountered an invalid internal state
227    CBKI001_INVALID_STATE,
228    /// CBKI002: Error budget exhausted; the reporter halted processing
229    /// after reaching the configured maximum error count
230    CBKI002_TOO_MANY_ERRORS,
231
232    // =============================================================================
233    // Encode Errors (CBKE*) - JSON to binary encoding validation
234    // =============================================================================
235    /// CBKE501: JSON value type doesn't match expected field type
236    CBKE501_JSON_TYPE_MISMATCH,
237    /// CBKE505: Decimal scale mismatch during field encoding
238    CBKE505_SCALE_MISMATCH,
239    /// CBKE510: Numeric value overflow for field capacity
240    CBKE510_NUMERIC_OVERFLOW,
241    /// CBKE515: String length exceeds field size limit
242    CBKE515_STRING_LENGTH_VIOLATION,
243    /// CBKE521: Array length exceeds ODO bounds
244    CBKE521_ARRAY_LEN_OOB,
245    /// CBKE530: SIGN SEPARATE encode error
246    CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
247    /// CBKE531: Float encode overflow (f64 value too large for f32 COMP-1 field)
248    CBKE531_FLOAT_ENCODE_OVERFLOW,
249
250    // =============================================================================
251    // File/Format Errors (CBKF*) - File structure and format validation
252    // =============================================================================
253    /// CBKF001: An input file could not be opened or read
254    CBKF001_FILE_READ_ERROR,
255    /// CBKF102: RDW length field references incomplete or oversized payload
256    CBKF102_RECORD_LENGTH_INVALID,
257    /// CBKF104: RDW appears to be corrupted by ASCII conversion
258    CBKF104_RDW_SUSPECT_ASCII,
259    /// CBKF221: RDW length field indicates underflow condition
260    CBKF221_RDW_UNDERFLOW,
261    /// CBKF222: BDW length field is zero, undersized, or oversized
262    CBKF222_BDW_LENGTH_INVALID,
263    /// CBKF223: BDW-declared block or nested RDW is truncated
264    CBKF223_BDW_UNDERFLOW,
265    /// CBKF224: nested RDW extends beyond its containing block
266    CBKF224_RDW_BEYOND_BLOCK,
267    /// CBKF225: BDW reserved bytes are nonzero under strict policy
268    CBKF225_BDW_RESERVED_NONZERO,
269
270    // =============================================================================
271    // Audit Errors (CBKA*) - Performance and compliance audit operations
272    // =============================================================================
273    /// CBKA001: Performance baseline operation error
274    CBKA001_BASELINE_ERROR,
275
276    // =============================================================================
277    // Arrow/Writer Errors (CBKW*) - Arrow and Parquet conversion errors
278    // =============================================================================
279    /// CBKW001: Failed COBOL to Arrow schema conversion
280    CBKW001_SCHEMA_CONVERSION,
281    /// CBKW002: `FieldKind` has no valid Arrow type mapping
282    CBKW002_TYPE_MAPPING,
283    /// CBKW003: Decimal precision exceeds Decimal128 limit (38 digits)
284    CBKW003_DECIMAL_OVERFLOW,
285    /// CBKW004: `RecordBatch` construction failure
286    CBKW004_BATCH_BUILD,
287    /// CBKW005: Parquet file write failure
288    CBKW005_PARQUET_WRITE,
289}
290
291impl fmt::Display for ErrorCode {
292    #[inline]
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        let code_str = match self {
295            ErrorCode::CBKP001_SYNTAX => "CBKP001_SYNTAX",
296            ErrorCode::CBKP011_UNSUPPORTED_CLAUSE => "CBKP011_UNSUPPORTED_CLAUSE",
297            ErrorCode::CBKP021_ODO_NOT_TAIL => "CBKP021_ODO_NOT_TAIL",
298            ErrorCode::CBKP022_NESTED_ODO => "CBKP022_NESTED_ODO",
299            ErrorCode::CBKP023_ODO_REDEFINES => "CBKP023_ODO_REDEFINES",
300            ErrorCode::CBKP051_UNSUPPORTED_EDITED_PIC => "CBKP051_UNSUPPORTED_EDITED_PIC",
301            ErrorCode::CBKP101_INVALID_PIC => "CBKP101_INVALID_PIC",
302            ErrorCode::CBKS121_COUNTER_NOT_FOUND => "CBKS121_COUNTER_NOT_FOUND",
303            ErrorCode::CBKS141_RECORD_TOO_LARGE => "CBKS141_RECORD_TOO_LARGE",
304            ErrorCode::CBKS301_ODO_CLIPPED => "CBKS301_ODO_CLIPPED",
305            ErrorCode::CBKS302_ODO_RAISED => "CBKS302_ODO_RAISED",
306            ErrorCode::CBKS601_RENAME_UNKNOWN_FROM => "CBKS601_RENAME_UNKNOWN_FROM",
307            ErrorCode::CBKS602_RENAME_UNKNOWN_THRU => "CBKS602_RENAME_UNKNOWN_THRU",
308            ErrorCode::CBKS603_RENAME_NOT_CONTIGUOUS => "CBKS603_RENAME_NOT_CONTIGUOUS",
309            ErrorCode::CBKS604_RENAME_REVERSED_RANGE => "CBKS604_RENAME_REVERSED_RANGE",
310            ErrorCode::CBKS605_RENAME_FROM_CROSSES_GROUP => "CBKS605_RENAME_FROM_CROSSES_GROUP",
311            ErrorCode::CBKS606_RENAME_THRU_CROSSES_GROUP => "CBKS606_RENAME_THRU_CROSSES_GROUP",
312            ErrorCode::CBKS607_RENAME_CROSSES_OCCURS => "CBKS607_RENAME_CROSSES_OCCURS",
313            ErrorCode::CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND => {
314                "CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND"
315            }
316            ErrorCode::CBKS609_RENAME_OVER_REDEFINES => "CBKS609_RENAME_OVER_REDEFINES",
317            ErrorCode::CBKS610_RENAME_MULTIPLE_REDEFINES => "CBKS610_RENAME_MULTIPLE_REDEFINES",
318            ErrorCode::CBKS611_RENAME_PARTIAL_OCCURS => "CBKS611_RENAME_PARTIAL_OCCURS",
319            ErrorCode::CBKS612_RENAME_ODO_NOT_SUPPORTED => "CBKS612_RENAME_ODO_NOT_SUPPORTED",
320            ErrorCode::CBKS701_PROJECTION_INVALID_ODO => "CBKS701_PROJECTION_INVALID_ODO",
321            ErrorCode::CBKS702_PROJECTION_UNRESOLVED_ALIAS => "CBKS702_PROJECTION_UNRESOLVED_ALIAS",
322            ErrorCode::CBKS703_PROJECTION_FIELD_NOT_FOUND => "CBKS703_PROJECTION_FIELD_NOT_FOUND",
323            ErrorCode::CBKR101_FIXED_RECORD_ERROR => "CBKR101_FIXED_RECORD_ERROR",
324            ErrorCode::CBKR201_RDW_READ_ERROR => "CBKR201_RDW_READ_ERROR",
325            ErrorCode::CBKR202_RDW_WRITE_ERROR => "CBKR202_RDW_WRITE_ERROR",
326            ErrorCode::CBKR211_RDW_RESERVED_NONZERO => "CBKR211_RDW_RESERVED_NONZERO",
327            ErrorCode::CBKC201_JSON_WRITE_ERROR => "CBKC201_JSON_WRITE_ERROR",
328            ErrorCode::CBKC301_INVALID_EBCDIC_BYTE => "CBKC301_INVALID_EBCDIC_BYTE",
329            ErrorCode::CBKD101_INVALID_FIELD_TYPE => "CBKD101_INVALID_FIELD_TYPE",
330            ErrorCode::CBKD301_RECORD_TOO_SHORT => "CBKD301_RECORD_TOO_SHORT",
331            ErrorCode::CBKD302_EDITED_PIC_NOT_IMPLEMENTED => "CBKD302_EDITED_PIC_NOT_IMPLEMENTED",
332            ErrorCode::CBKD401_COMP3_INVALID_NIBBLE => "CBKD401_COMP3_INVALID_NIBBLE",
333            ErrorCode::CBKD410_ZONED_OVERFLOW => "CBKD410_ZONED_OVERFLOW",
334            ErrorCode::CBKD411_ZONED_BAD_SIGN => "CBKD411_ZONED_BAD_SIGN",
335            ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO => "CBKD412_ZONED_BLANK_IS_ZERO",
336            ErrorCode::CBKD413_ZONED_INVALID_ENCODING => "CBKD413_ZONED_INVALID_ENCODING",
337            ErrorCode::CBKD414_ZONED_MIXED_ENCODING => "CBKD414_ZONED_MIXED_ENCODING",
338            ErrorCode::CBKD415_ZONED_ENCODING_AMBIGUOUS => "CBKD415_ZONED_ENCODING_AMBIGUOUS",
339            ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT => "CBKD421_EDITED_PIC_INVALID_FORMAT",
340            ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH => "CBKD422_EDITED_PIC_SIGN_MISMATCH",
341            ErrorCode::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO => "CBKD423_EDITED_PIC_BLANK_WHEN_ZERO",
342            ErrorCode::CBKD431_FLOAT_NAN => "CBKD431_FLOAT_NAN",
343            ErrorCode::CBKD432_FLOAT_INFINITY => "CBKD432_FLOAT_INFINITY",
344            ErrorCode::CBKI001_INVALID_STATE => "CBKI001_INVALID_STATE",
345            ErrorCode::CBKI002_TOO_MANY_ERRORS => "CBKI002_TOO_MANY_ERRORS",
346            ErrorCode::CBKE501_JSON_TYPE_MISMATCH => "CBKE501_JSON_TYPE_MISMATCH",
347            ErrorCode::CBKE505_SCALE_MISMATCH => "CBKE505_SCALE_MISMATCH",
348            ErrorCode::CBKE510_NUMERIC_OVERFLOW => "CBKE510_NUMERIC_OVERFLOW",
349            ErrorCode::CBKE515_STRING_LENGTH_VIOLATION => "CBKE515_STRING_LENGTH_VIOLATION",
350            ErrorCode::CBKE521_ARRAY_LEN_OOB => "CBKE521_ARRAY_LEN_OOB",
351            ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR => "CBKE530_SIGN_SEPARATE_ENCODE_ERROR",
352            ErrorCode::CBKE531_FLOAT_ENCODE_OVERFLOW => "CBKE531_FLOAT_ENCODE_OVERFLOW",
353            ErrorCode::CBKF001_FILE_READ_ERROR => "CBKF001_FILE_READ_ERROR",
354            ErrorCode::CBKF102_RECORD_LENGTH_INVALID => "CBKF102_RECORD_LENGTH_INVALID",
355            ErrorCode::CBKF104_RDW_SUSPECT_ASCII => "CBKF104_RDW_SUSPECT_ASCII",
356            ErrorCode::CBKF221_RDW_UNDERFLOW => "CBKF221_RDW_UNDERFLOW",
357            ErrorCode::CBKF222_BDW_LENGTH_INVALID => "CBKF222_BDW_LENGTH_INVALID",
358            ErrorCode::CBKF223_BDW_UNDERFLOW => "CBKF223_BDW_UNDERFLOW",
359            ErrorCode::CBKF224_RDW_BEYOND_BLOCK => "CBKF224_RDW_BEYOND_BLOCK",
360            ErrorCode::CBKF225_BDW_RESERVED_NONZERO => "CBKF225_BDW_RESERVED_NONZERO",
361            ErrorCode::CBKA001_BASELINE_ERROR => "CBKA001_BASELINE_ERROR",
362            ErrorCode::CBKW001_SCHEMA_CONVERSION => "CBKW001_SCHEMA_CONVERSION",
363            ErrorCode::CBKW002_TYPE_MAPPING => "CBKW002_TYPE_MAPPING",
364            ErrorCode::CBKW003_DECIMAL_OVERFLOW => "CBKW003_DECIMAL_OVERFLOW",
365            ErrorCode::CBKW004_BATCH_BUILD => "CBKW004_BATCH_BUILD",
366            ErrorCode::CBKW005_PARQUET_WRITE => "CBKW005_PARQUET_WRITE",
367        };
368        write!(f, "{code_str}")
369    }
370}
371
372impl ErrorCode {
373    /// Return the 4-character family prefix (e.g., `CBKD`) for this error code.
374    #[inline]
375    #[must_use]
376    pub const fn family_prefix(self) -> &'static str {
377        match self {
378            Self::CBKP001_SYNTAX
379            | Self::CBKP011_UNSUPPORTED_CLAUSE
380            | Self::CBKP021_ODO_NOT_TAIL
381            | Self::CBKP022_NESTED_ODO
382            | Self::CBKP023_ODO_REDEFINES
383            | Self::CBKP051_UNSUPPORTED_EDITED_PIC
384            | Self::CBKP101_INVALID_PIC => "CBKP",
385            Self::CBKS121_COUNTER_NOT_FOUND
386            | Self::CBKS141_RECORD_TOO_LARGE
387            | Self::CBKS301_ODO_CLIPPED
388            | Self::CBKS302_ODO_RAISED
389            | Self::CBKS601_RENAME_UNKNOWN_FROM
390            | Self::CBKS602_RENAME_UNKNOWN_THRU
391            | Self::CBKS603_RENAME_NOT_CONTIGUOUS
392            | Self::CBKS604_RENAME_REVERSED_RANGE
393            | Self::CBKS605_RENAME_FROM_CROSSES_GROUP
394            | Self::CBKS606_RENAME_THRU_CROSSES_GROUP
395            | Self::CBKS607_RENAME_CROSSES_OCCURS
396            | Self::CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND
397            | Self::CBKS609_RENAME_OVER_REDEFINES
398            | Self::CBKS610_RENAME_MULTIPLE_REDEFINES
399            | Self::CBKS611_RENAME_PARTIAL_OCCURS
400            | Self::CBKS612_RENAME_ODO_NOT_SUPPORTED
401            | Self::CBKS701_PROJECTION_INVALID_ODO
402            | Self::CBKS702_PROJECTION_UNRESOLVED_ALIAS
403            | Self::CBKS703_PROJECTION_FIELD_NOT_FOUND => "CBKS",
404            Self::CBKR101_FIXED_RECORD_ERROR
405            | Self::CBKR201_RDW_READ_ERROR
406            | Self::CBKR202_RDW_WRITE_ERROR
407            | Self::CBKR211_RDW_RESERVED_NONZERO => "CBKR",
408            Self::CBKC201_JSON_WRITE_ERROR | Self::CBKC301_INVALID_EBCDIC_BYTE => "CBKC",
409            Self::CBKD101_INVALID_FIELD_TYPE
410            | Self::CBKD301_RECORD_TOO_SHORT
411            | Self::CBKD302_EDITED_PIC_NOT_IMPLEMENTED
412            | Self::CBKD401_COMP3_INVALID_NIBBLE
413            | Self::CBKD410_ZONED_OVERFLOW
414            | Self::CBKD411_ZONED_BAD_SIGN
415            | Self::CBKD412_ZONED_BLANK_IS_ZERO
416            | Self::CBKD413_ZONED_INVALID_ENCODING
417            | Self::CBKD414_ZONED_MIXED_ENCODING
418            | Self::CBKD415_ZONED_ENCODING_AMBIGUOUS
419            | Self::CBKD421_EDITED_PIC_INVALID_FORMAT
420            | Self::CBKD422_EDITED_PIC_SIGN_MISMATCH
421            | Self::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO
422            | Self::CBKD431_FLOAT_NAN
423            | Self::CBKD432_FLOAT_INFINITY => "CBKD",
424            Self::CBKI001_INVALID_STATE | Self::CBKI002_TOO_MANY_ERRORS => "CBKI",
425            Self::CBKE501_JSON_TYPE_MISMATCH
426            | Self::CBKE505_SCALE_MISMATCH
427            | Self::CBKE510_NUMERIC_OVERFLOW
428            | Self::CBKE515_STRING_LENGTH_VIOLATION
429            | Self::CBKE521_ARRAY_LEN_OOB
430            | Self::CBKE530_SIGN_SEPARATE_ENCODE_ERROR
431            | Self::CBKE531_FLOAT_ENCODE_OVERFLOW => "CBKE",
432            Self::CBKF001_FILE_READ_ERROR
433            | Self::CBKF102_RECORD_LENGTH_INVALID
434            | Self::CBKF104_RDW_SUSPECT_ASCII
435            | Self::CBKF221_RDW_UNDERFLOW
436            | Self::CBKF222_BDW_LENGTH_INVALID
437            | Self::CBKF223_BDW_UNDERFLOW
438            | Self::CBKF224_RDW_BEYOND_BLOCK
439            | Self::CBKF225_BDW_RESERVED_NONZERO => "CBKF",
440            Self::CBKA001_BASELINE_ERROR => "CBKA",
441            Self::CBKW001_SCHEMA_CONVERSION
442            | Self::CBKW002_TYPE_MAPPING
443            | Self::CBKW003_DECIMAL_OVERFLOW
444            | Self::CBKW004_BATCH_BUILD
445            | Self::CBKW005_PARQUET_WRITE => "CBKW",
446        }
447    }
448}
449
450/// Context information for detailed error reporting
451///
452/// Provides comprehensive location and contextual information for errors,
453/// enabling precise error reporting and debugging in enterprise environments.
454/// All fields are optional to accommodate different error scenarios.
455#[derive(Debug, Clone, PartialEq)]
456pub struct ErrorContext {
457    /// Record number (1-based) where the error occurred
458    ///
459    /// Used for data processing errors to identify the specific record
460    /// in multi-record files or streams.
461    pub record_index: Option<u64>,
462
463    /// Hierarchical field path where the error occurred
464    ///
465    /// Uses dot notation (e.g., "customer.address.street") to identify
466    /// the exact field location within nested structures.
467    pub field_path: Option<String>,
468
469    /// Byte offset within the record or file where the error occurred
470    ///
471    /// Provides precise location information for debugging binary data issues.
472    pub byte_offset: Option<u64>,
473
474    /// Line number in the copybook source (for parse errors)
475    ///
476    /// Used during copybook parsing to identify problematic COBOL syntax.
477    pub line_number: Option<u32>,
478
479    /// Additional context-specific information
480    ///
481    /// Free-form text providing extra details relevant to the specific error.
482    pub details: Option<String>,
483}
484
485impl fmt::Display for ErrorContext {
486    #[inline]
487    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488        let mut parts = Vec::new();
489
490        if let Some(record) = self.record_index {
491            parts.push(format!("record {record}"));
492        }
493        if let Some(ref path) = self.field_path {
494            parts.push(format!("field {path}"));
495        }
496        if let Some(offset) = self.byte_offset {
497            parts.push(format!("offset {offset}"));
498        }
499        if let Some(line) = self.line_number {
500            parts.push(format!("line {line}"));
501        }
502        if let Some(ref details) = self.details {
503            parts.push(details.clone());
504        }
505
506        write!(f, "{}", parts.join(", "))
507    }
508}
509
510impl Error {
511    /// Create a new error with the specified code and message
512    ///
513    /// This is the primary constructor for copybook-rs errors. The error code
514    /// should be chosen from the stable `ErrorCode` taxonomy to enable
515    /// programmatic error handling.
516    ///
517    /// # Arguments
518    /// * `code` - Stable error code from the copybook-rs taxonomy
519    /// * `message` - Human-readable error description
520    ///
521    /// # Example
522    /// ```rust
523    /// use copybook_error::{Error, ErrorCode};
524    ///
525    /// // Static message
526    /// let error1 = Error::new(
527    ///     ErrorCode::CBKD411_ZONED_BAD_SIGN,
528    ///     "Invalid sign zone in zoned decimal field"
529    /// );
530    ///
531    /// // Dynamic message
532    /// let field_name = "AMOUNT";
533    /// let error2 = Error::new(
534    ///     ErrorCode::CBKD411_ZONED_BAD_SIGN,
535    ///     format!("Invalid sign zone in field {}", field_name)
536    /// );
537    /// ```
538    #[inline]
539    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
540        Self {
541            code,
542            message: message.into(),
543            context: None,
544        }
545    }
546
547    /// Add context information to the error
548    #[must_use]
549    #[inline]
550    pub fn with_context(mut self, context: ErrorContext) -> Self {
551        self.context = Some(context);
552        self
553    }
554
555    /// Add record context to the error
556    #[must_use]
557    #[inline]
558    pub fn with_record(mut self, record_index: u64) -> Self {
559        let context = self.context.get_or_insert(ErrorContext {
560            record_index: None,
561            field_path: None,
562            byte_offset: None,
563            line_number: None,
564            details: None,
565        });
566        context.record_index = Some(record_index);
567        self
568    }
569
570    /// Add field path context to the error
571    #[must_use]
572    #[inline]
573    pub fn with_field(mut self, field_path: impl Into<String>) -> Self {
574        let context = self.context.get_or_insert(ErrorContext {
575            record_index: None,
576            field_path: None,
577            byte_offset: None,
578            line_number: None,
579            details: None,
580        });
581        context.field_path = Some(field_path.into());
582        self
583    }
584
585    /// Add byte offset context to the error
586    #[must_use]
587    #[inline]
588    pub fn with_offset(mut self, byte_offset: u64) -> Self {
589        let context = self.context.get_or_insert(ErrorContext {
590            record_index: None,
591            field_path: None,
592            byte_offset: None,
593            line_number: None,
594            details: None,
595        });
596        context.byte_offset = Some(byte_offset);
597        self
598    }
599}
600
601/// Convenience macros for creating errors
602#[macro_export]
603macro_rules! error {
604    ($code:expr, $msg:expr) => {
605        $crate::Error::new($code, $msg)
606    };
607    ($code:expr, $fmt:expr, $($arg:tt)*) => {
608        $crate::Error::new($code, format!($fmt, $($arg)*))
609    };
610}
611
612#[cfg(test)]
613#[allow(clippy::expect_used)]
614#[allow(clippy::unwrap_used)] // Allow unwrap in tests for brevity
615mod tests {
616    use super::*;
617
618    #[test]
619    fn test_error_code_serialization() {
620        let code = ErrorCode::CBKD411_ZONED_BAD_SIGN;
621        let json = serde_json::to_string(&code).unwrap();
622        assert_eq!(json, "\"CBKD411_ZONED_BAD_SIGN\"");
623
624        let deserialized: ErrorCode = serde_json::from_str(&json).unwrap();
625        assert_eq!(deserialized, code);
626    }
627
628    #[test]
629    fn test_error_display_format() {
630        let error = Error::new(
631            ErrorCode::CBKD411_ZONED_BAD_SIGN,
632            "Invalid sign zone in field",
633        );
634        let display = format!("{error}");
635        assert_eq!(
636            display,
637            "CBKD411_ZONED_BAD_SIGN: Invalid sign zone in field"
638        );
639    }
640
641    #[test]
642    fn test_error_with_context_display() {
643        let error =
644            Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "Test error").with_field("AMOUNT");
645        let display = format!("{error}");
646        assert!(display.contains("CBKD411_ZONED_BAD_SIGN: Test error"));
647        assert!(display.contains("field AMOUNT"));
648    }
649
650    #[test]
651    fn test_error_static_message() {
652        let error = Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "Static message");
653        assert_eq!(error.message, "Static message");
654        assert_eq!(error.code, ErrorCode::CBKD411_ZONED_BAD_SIGN);
655        assert!(error.context.is_none());
656    }
657
658    #[test]
659    fn test_error_dynamic_message() {
660        let field = "AMOUNT";
661        let error = Error::new(
662            ErrorCode::CBKD411_ZONED_BAD_SIGN,
663            format!("Dynamic message for field {field}"),
664        );
665        assert_eq!(error.message, "Dynamic message for field AMOUNT");
666    }
667
668    #[test]
669    fn test_error_macro_static() {
670        let err = error!(ErrorCode::CBKP001_SYNTAX, "Empty copybook");
671        assert_eq!(err.code, ErrorCode::CBKP001_SYNTAX);
672        assert_eq!(err.message, "Empty copybook");
673    }
674
675    #[test]
676    fn test_error_macro_formatted() {
677        let field = "CUSTOMER_ID";
678        let err = error!(
679            ErrorCode::CBKD301_RECORD_TOO_SHORT,
680            "Field {} missing", field
681        );
682        assert_eq!(err.code, ErrorCode::CBKD301_RECORD_TOO_SHORT);
683        assert!(err.message.contains("CUSTOMER_ID"));
684    }
685
686    // -----------------------------------------------------------------------
687    // Error accessor and builder tests
688    // -----------------------------------------------------------------------
689
690    #[test]
691    fn test_error_code_accessor() {
692        let err = Error::new(ErrorCode::CBKE510_NUMERIC_OVERFLOW, "overflow");
693        assert_eq!(err.code(), ErrorCode::CBKE510_NUMERIC_OVERFLOW);
694    }
695
696    #[test]
697    fn test_error_family_prefix_via_error() {
698        let err = Error::new(ErrorCode::CBKR211_RDW_RESERVED_NONZERO, "reserved");
699        assert_eq!(err.family_prefix(), "CBKR");
700    }
701
702    #[test]
703    fn test_error_with_record_builder() {
704        let err = Error::new(ErrorCode::CBKD301_RECORD_TOO_SHORT, "short").with_record(7);
705        let ctx = err.context.as_ref().unwrap();
706        assert_eq!(ctx.record_index, Some(7));
707        assert!(ctx.field_path.is_none());
708        assert!(ctx.byte_offset.is_none());
709    }
710
711    #[test]
712    fn test_error_with_offset_builder() {
713        let err = Error::new(ErrorCode::CBKD301_RECORD_TOO_SHORT, "short").with_offset(128);
714        let ctx = err.context.as_ref().unwrap();
715        assert_eq!(ctx.byte_offset, Some(128));
716        assert!(ctx.record_index.is_none());
717        assert!(ctx.field_path.is_none());
718    }
719
720    #[test]
721    fn test_error_chained_context_builders() {
722        let err = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "bad nibble")
723            .with_record(10)
724            .with_field("CUSTOMER.BALANCE")
725            .with_offset(64);
726        let ctx = err.context.as_ref().unwrap();
727        assert_eq!(ctx.record_index, Some(10));
728        assert_eq!(ctx.field_path.as_deref(), Some("CUSTOMER.BALANCE"));
729        assert_eq!(ctx.byte_offset, Some(64));
730        assert!(ctx.line_number.is_none());
731        assert!(ctx.details.is_none());
732    }
733
734    #[test]
735    fn test_error_with_context_sets_full_context() {
736        let ctx = ErrorContext {
737            record_index: Some(99),
738            field_path: Some("ROOT.CHILD".into()),
739            byte_offset: Some(512),
740            line_number: Some(42),
741            details: Some("extra detail".into()),
742        };
743        let err = Error::new(ErrorCode::CBKP001_SYNTAX, "bad syntax").with_context(ctx);
744        let c = err.context.as_ref().unwrap();
745        assert_eq!(c.record_index, Some(99));
746        assert_eq!(c.field_path.as_deref(), Some("ROOT.CHILD"));
747        assert_eq!(c.byte_offset, Some(512));
748        assert_eq!(c.line_number, Some(42));
749        assert_eq!(c.details.as_deref(), Some("extra detail"));
750    }
751
752    // -----------------------------------------------------------------------
753    // Trait implementation tests
754    // -----------------------------------------------------------------------
755
756    #[test]
757    fn test_error_clone_equality() {
758        let err1 = Error::new(ErrorCode::CBKE501_JSON_TYPE_MISMATCH, "mismatch")
759            .with_field("AMOUNT")
760            .with_record(3);
761        let err2 = err1.clone();
762        assert_eq!(err1, err2);
763        assert_eq!(err1.code, err2.code);
764        assert_eq!(err1.message, err2.message);
765        assert_eq!(err1.context, err2.context);
766    }
767
768    #[test]
769    fn test_error_code_copy_clone_hash() {
770        use std::collections::HashSet;
771        let code = ErrorCode::CBKP001_SYNTAX;
772        let copy = code;
773        assert_eq!(code, copy);
774
775        let mut set = HashSet::new();
776        set.insert(ErrorCode::CBKP001_SYNTAX);
777        set.insert(ErrorCode::CBKD301_RECORD_TOO_SHORT);
778        set.insert(ErrorCode::CBKP001_SYNTAX); // duplicate
779        assert_eq!(set.len(), 2);
780    }
781
782    #[test]
783    fn test_error_implements_std_error() {
784        let err = Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "bad sign");
785        let std_err: &dyn std::error::Error = &err;
786        // source() should be None since Error has no #[source] field
787        assert!(std_err.source().is_none());
788        assert!(!std_err.to_string().is_empty());
789    }
790
791    // -----------------------------------------------------------------------
792    // ErrorContext Display edge cases
793    // -----------------------------------------------------------------------
794
795    #[test]
796    fn test_error_context_display_empty() {
797        let ctx = ErrorContext {
798            record_index: None,
799            field_path: None,
800            byte_offset: None,
801            line_number: None,
802            details: None,
803        };
804        assert_eq!(format!("{ctx}"), "");
805    }
806
807    #[test]
808    fn test_error_context_display_record_only() {
809        let ctx = ErrorContext {
810            record_index: Some(42),
811            field_path: None,
812            byte_offset: None,
813            line_number: None,
814            details: None,
815        };
816        assert_eq!(format!("{ctx}"), "record 42");
817    }
818
819    #[test]
820    fn test_error_context_display_line_number_only() {
821        let ctx = ErrorContext {
822            record_index: None,
823            field_path: None,
824            byte_offset: None,
825            line_number: Some(15),
826            details: None,
827        };
828        assert_eq!(format!("{ctx}"), "line 15");
829    }
830
831    #[test]
832    fn test_error_context_display_details_only() {
833        let ctx = ErrorContext {
834            record_index: None,
835            field_path: None,
836            byte_offset: None,
837            line_number: None,
838            details: Some("expected 8 bytes, got 4".into()),
839        };
840        assert_eq!(format!("{ctx}"), "expected 8 bytes, got 4");
841    }
842
843    // -----------------------------------------------------------------------
844    // ErrorCode family_prefix exhaustive coverage
845    // -----------------------------------------------------------------------
846
847    #[test]
848    fn test_all_cbkp_codes_have_cbkp_prefix() {
849        let codes = [
850            ErrorCode::CBKP001_SYNTAX,
851            ErrorCode::CBKP011_UNSUPPORTED_CLAUSE,
852            ErrorCode::CBKP021_ODO_NOT_TAIL,
853            ErrorCode::CBKP022_NESTED_ODO,
854            ErrorCode::CBKP023_ODO_REDEFINES,
855            ErrorCode::CBKP051_UNSUPPORTED_EDITED_PIC,
856            ErrorCode::CBKP101_INVALID_PIC,
857        ];
858        for code in codes {
859            assert_eq!(code.family_prefix(), "CBKP", "failed for {code}");
860        }
861    }
862
863    #[test]
864    fn test_all_cbks_codes_have_cbks_prefix() {
865        let codes = [
866            ErrorCode::CBKS121_COUNTER_NOT_FOUND,
867            ErrorCode::CBKS141_RECORD_TOO_LARGE,
868            ErrorCode::CBKS301_ODO_CLIPPED,
869            ErrorCode::CBKS302_ODO_RAISED,
870            ErrorCode::CBKS601_RENAME_UNKNOWN_FROM,
871            ErrorCode::CBKS602_RENAME_UNKNOWN_THRU,
872            ErrorCode::CBKS603_RENAME_NOT_CONTIGUOUS,
873            ErrorCode::CBKS604_RENAME_REVERSED_RANGE,
874            ErrorCode::CBKS605_RENAME_FROM_CROSSES_GROUP,
875            ErrorCode::CBKS606_RENAME_THRU_CROSSES_GROUP,
876            ErrorCode::CBKS607_RENAME_CROSSES_OCCURS,
877            ErrorCode::CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND,
878            ErrorCode::CBKS609_RENAME_OVER_REDEFINES,
879            ErrorCode::CBKS610_RENAME_MULTIPLE_REDEFINES,
880            ErrorCode::CBKS611_RENAME_PARTIAL_OCCURS,
881            ErrorCode::CBKS612_RENAME_ODO_NOT_SUPPORTED,
882            ErrorCode::CBKS701_PROJECTION_INVALID_ODO,
883            ErrorCode::CBKS702_PROJECTION_UNRESOLVED_ALIAS,
884            ErrorCode::CBKS703_PROJECTION_FIELD_NOT_FOUND,
885        ];
886        for code in codes {
887            assert_eq!(code.family_prefix(), "CBKS", "failed for {code}");
888        }
889    }
890
891    #[test]
892    fn test_all_cbkd_codes_have_cbkd_prefix() {
893        let codes = [
894            ErrorCode::CBKD101_INVALID_FIELD_TYPE,
895            ErrorCode::CBKD301_RECORD_TOO_SHORT,
896            ErrorCode::CBKD302_EDITED_PIC_NOT_IMPLEMENTED,
897            ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
898            ErrorCode::CBKD410_ZONED_OVERFLOW,
899            ErrorCode::CBKD411_ZONED_BAD_SIGN,
900            ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO,
901            ErrorCode::CBKD413_ZONED_INVALID_ENCODING,
902            ErrorCode::CBKD414_ZONED_MIXED_ENCODING,
903            ErrorCode::CBKD415_ZONED_ENCODING_AMBIGUOUS,
904            ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
905            ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
906            ErrorCode::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO,
907            ErrorCode::CBKD431_FLOAT_NAN,
908            ErrorCode::CBKD432_FLOAT_INFINITY,
909        ];
910        for code in codes {
911            assert_eq!(code.family_prefix(), "CBKD", "failed for {code}");
912        }
913    }
914
915    #[test]
916    fn test_all_cbke_codes_have_cbke_prefix() {
917        let codes = [
918            ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
919            ErrorCode::CBKE505_SCALE_MISMATCH,
920            ErrorCode::CBKE510_NUMERIC_OVERFLOW,
921            ErrorCode::CBKE515_STRING_LENGTH_VIOLATION,
922            ErrorCode::CBKE521_ARRAY_LEN_OOB,
923            ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
924            ErrorCode::CBKE531_FLOAT_ENCODE_OVERFLOW,
925        ];
926        for code in codes {
927            assert_eq!(code.family_prefix(), "CBKE", "failed for {code}");
928        }
929    }
930
931    #[test]
932    fn test_remaining_family_prefixes() {
933        assert_eq!(
934            ErrorCode::CBKR211_RDW_RESERVED_NONZERO.family_prefix(),
935            "CBKR"
936        );
937        assert_eq!(ErrorCode::CBKC201_JSON_WRITE_ERROR.family_prefix(), "CBKC");
938        assert_eq!(
939            ErrorCode::CBKC301_INVALID_EBCDIC_BYTE.family_prefix(),
940            "CBKC"
941        );
942        assert_eq!(ErrorCode::CBKI001_INVALID_STATE.family_prefix(), "CBKI");
943        assert_eq!(ErrorCode::CBKI002_TOO_MANY_ERRORS.family_prefix(), "CBKI");
944        assert_eq!(
945            ErrorCode::CBKF102_RECORD_LENGTH_INVALID.family_prefix(),
946            "CBKF"
947        );
948        assert_eq!(ErrorCode::CBKF104_RDW_SUSPECT_ASCII.family_prefix(), "CBKF");
949        assert_eq!(ErrorCode::CBKF221_RDW_UNDERFLOW.family_prefix(), "CBKF");
950        assert_eq!(ErrorCode::CBKA001_BASELINE_ERROR.family_prefix(), "CBKA");
951        assert_eq!(ErrorCode::CBKW001_SCHEMA_CONVERSION.family_prefix(), "CBKW");
952        assert_eq!(ErrorCode::CBKW002_TYPE_MAPPING.family_prefix(), "CBKW");
953        assert_eq!(ErrorCode::CBKW003_DECIMAL_OVERFLOW.family_prefix(), "CBKW");
954        assert_eq!(ErrorCode::CBKW004_BATCH_BUILD.family_prefix(), "CBKW");
955        assert_eq!(ErrorCode::CBKW005_PARQUET_WRITE.family_prefix(), "CBKW");
956    }
957
958    // -----------------------------------------------------------------------
959    // ErrorCode Display consistency: all codes display as variant name
960    // -----------------------------------------------------------------------
961
962    #[test]
963    fn test_error_code_display_starts_with_family_prefix() {
964        let representative_codes = [
965            ErrorCode::CBKP001_SYNTAX,
966            ErrorCode::CBKS121_COUNTER_NOT_FOUND,
967            ErrorCode::CBKR211_RDW_RESERVED_NONZERO,
968            ErrorCode::CBKC201_JSON_WRITE_ERROR,
969            ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
970            ErrorCode::CBKI001_INVALID_STATE,
971            ErrorCode::CBKI002_TOO_MANY_ERRORS,
972            ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
973            ErrorCode::CBKF102_RECORD_LENGTH_INVALID,
974            ErrorCode::CBKA001_BASELINE_ERROR,
975            ErrorCode::CBKW001_SCHEMA_CONVERSION,
976        ];
977        for code in representative_codes {
978            let display = format!("{code}");
979            let prefix = code.family_prefix();
980            assert!(
981                display.starts_with(prefix),
982                "{display} should start with {prefix}"
983            );
984        }
985    }
986
987    // -----------------------------------------------------------------------
988    // Serde round-trip for each error code family
989    // -----------------------------------------------------------------------
990
991    #[test]
992    fn test_error_code_serde_roundtrip_all_families() {
993        let codes = [
994            ErrorCode::CBKP001_SYNTAX,
995            ErrorCode::CBKS121_COUNTER_NOT_FOUND,
996            ErrorCode::CBKR211_RDW_RESERVED_NONZERO,
997            ErrorCode::CBKC201_JSON_WRITE_ERROR,
998            ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
999            ErrorCode::CBKI001_INVALID_STATE,
1000            ErrorCode::CBKI002_TOO_MANY_ERRORS,
1001            ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
1002            ErrorCode::CBKF102_RECORD_LENGTH_INVALID,
1003            ErrorCode::CBKA001_BASELINE_ERROR,
1004            ErrorCode::CBKW001_SCHEMA_CONVERSION,
1005        ];
1006        for code in codes {
1007            let json = serde_json::to_string(&code).unwrap();
1008            let roundtripped: ErrorCode = serde_json::from_str(&json).unwrap();
1009            assert_eq!(roundtripped, code, "round-trip failed for {code}");
1010        }
1011    }
1012
1013    #[test]
1014    fn test_error_code_deserialization_from_string() {
1015        let json = r#""CBKP101_INVALID_PIC""#;
1016        let code: ErrorCode = serde_json::from_str(json).unwrap();
1017        assert_eq!(code, ErrorCode::CBKP101_INVALID_PIC);
1018    }
1019
1020    #[test]
1021    fn test_error_code_deserialization_invalid_rejects() {
1022        let json = r#""NOT_A_REAL_CODE""#;
1023        let result: std::result::Result<ErrorCode, _> = serde_json::from_str(json);
1024        assert!(result.is_err());
1025    }
1026
1027    // -----------------------------------------------------------------------
1028    // error! macro with multiple format args
1029    // -----------------------------------------------------------------------
1030
1031    #[test]
1032    fn test_error_macro_multiple_format_args() {
1033        let field = "AMOUNT";
1034        let expected = 8;
1035        let actual = 4;
1036        let err = error!(
1037            ErrorCode::CBKD301_RECORD_TOO_SHORT,
1038            "Field {} expected {} bytes, got {}", field, expected, actual
1039        );
1040        assert_eq!(err.message, "Field AMOUNT expected 8 bytes, got 4");
1041    }
1042}