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