1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2use serde::{Deserialize, Serialize};
9use std::fmt;
10use thiserror::Error;
11
12pub type Result<T> = std::result::Result<T, Error>;
14
15#[derive(Error, Debug, Clone, PartialEq)]
31pub struct Error {
32 pub code: ErrorCode,
34
35 pub message: String,
37
38 pub context: Option<ErrorContext>,
40}
41
42impl 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 #[inline]
57 #[must_use]
58 pub const fn code(&self) -> ErrorCode {
59 self.code
60 }
61
62 #[inline]
64 #[must_use]
65 pub const fn family_prefix(&self) -> &'static str {
66 self.code.family_prefix()
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
102#[allow(non_camel_case_types)] pub enum ErrorCode {
104 CBKP001_SYNTAX,
109 CBKP011_UNSUPPORTED_CLAUSE,
111 CBKP021_ODO_NOT_TAIL,
113 CBKP022_NESTED_ODO,
115 CBKP023_ODO_REDEFINES,
117 CBKP051_UNSUPPORTED_EDITED_PIC,
119 CBKP101_INVALID_PIC,
121
122 CBKS121_COUNTER_NOT_FOUND,
127 CBKS141_RECORD_TOO_LARGE,
129 CBKS301_ODO_CLIPPED,
131 CBKS302_ODO_RAISED,
133 CBKS601_RENAME_UNKNOWN_FROM,
135 CBKS602_RENAME_UNKNOWN_THRU,
137 CBKS603_RENAME_NOT_CONTIGUOUS,
139 CBKS604_RENAME_REVERSED_RANGE,
141 CBKS605_RENAME_FROM_CROSSES_GROUP,
143 CBKS606_RENAME_THRU_CROSSES_GROUP,
145 CBKS607_RENAME_CROSSES_OCCURS,
147 CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND,
149 CBKS609_RENAME_OVER_REDEFINES,
151 CBKS610_RENAME_MULTIPLE_REDEFINES,
153 CBKS611_RENAME_PARTIAL_OCCURS,
155 CBKS612_RENAME_ODO_NOT_SUPPORTED,
157 CBKS701_PROJECTION_INVALID_ODO,
159 CBKS702_PROJECTION_UNRESOLVED_ALIAS,
161 CBKS703_PROJECTION_FIELD_NOT_FOUND,
163
164 CBKR101_FIXED_RECORD_ERROR,
169 CBKR201_RDW_READ_ERROR,
171 CBKR202_RDW_WRITE_ERROR,
174 CBKR211_RDW_RESERVED_NONZERO,
176
177 CBKC201_JSON_WRITE_ERROR,
182 CBKC301_INVALID_EBCDIC_BYTE,
184
185 CBKD101_INVALID_FIELD_TYPE,
190 CBKD301_RECORD_TOO_SHORT,
192 CBKD302_EDITED_PIC_NOT_IMPLEMENTED,
194 CBKD401_COMP3_INVALID_NIBBLE,
196 CBKD410_ZONED_OVERFLOW,
198 CBKD411_ZONED_BAD_SIGN,
200 CBKD412_ZONED_BLANK_IS_ZERO,
202 CBKD413_ZONED_INVALID_ENCODING,
204 CBKD414_ZONED_MIXED_ENCODING,
206 CBKD415_ZONED_ENCODING_AMBIGUOUS,
208 CBKD421_EDITED_PIC_INVALID_FORMAT,
210 CBKD422_EDITED_PIC_SIGN_MISMATCH,
212 CBKD423_EDITED_PIC_BLANK_WHEN_ZERO,
214 CBKD431_FLOAT_NAN,
218 CBKD432_FLOAT_INFINITY,
222
223 CBKI001_INVALID_STATE,
228 CBKI002_TOO_MANY_ERRORS,
231
232 CBKE501_JSON_TYPE_MISMATCH,
237 CBKE505_SCALE_MISMATCH,
239 CBKE510_NUMERIC_OVERFLOW,
241 CBKE515_STRING_LENGTH_VIOLATION,
243 CBKE521_ARRAY_LEN_OOB,
245 CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
247 CBKE531_FLOAT_ENCODE_OVERFLOW,
249
250 CBKF001_FILE_READ_ERROR,
255 CBKF102_RECORD_LENGTH_INVALID,
257 CBKF104_RDW_SUSPECT_ASCII,
259 CBKF221_RDW_UNDERFLOW,
261 CBKF222_BDW_LENGTH_INVALID,
263 CBKF223_BDW_UNDERFLOW,
265 CBKF224_RDW_BEYOND_BLOCK,
267 CBKF225_BDW_RESERVED_NONZERO,
269
270 CBKA001_BASELINE_ERROR,
275
276 CBKW001_SCHEMA_CONVERSION,
281 CBKW002_TYPE_MAPPING,
283 CBKW003_DECIMAL_OVERFLOW,
285 CBKW004_BATCH_BUILD,
287 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 #[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#[derive(Debug, Clone, PartialEq)]
456pub struct ErrorContext {
457 pub record_index: Option<u64>,
462
463 pub field_path: Option<String>,
468
469 pub byte_offset: Option<u64>,
473
474 pub line_number: Option<u32>,
478
479 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 #[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 #[must_use]
549 #[inline]
550 pub fn with_context(mut self, context: ErrorContext) -> Self {
551 self.context = Some(context);
552 self
553 }
554
555 #[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 #[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 #[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#[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)] mod 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 #[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 #[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); 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 assert!(std_err.source().is_none());
788 assert!(!std_err.to_string().is_empty());
789 }
790
791 #[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 #[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 #[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 #[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 #[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}