1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2#![allow(clippy::missing_inline_in_public_items)]
9
10pub use copybook_charset::Codepage;
13pub use copybook_charset::UnmappablePolicy;
15pub use copybook_zoned_format::ZonedEncodingFormat;
17use serde::{Deserialize, Serialize};
18use std::fmt;
19
20const DEFAULT_RECORD_FORMAT: RecordFormat = RecordFormat::Fixed;
21const DEFAULT_CODEPAGE: Codepage = Codepage::CP037;
22const DEFAULT_JSON_NUMBER_MODE: JsonNumberMode = JsonNumberMode::Lossless;
23const DEFAULT_UNMAPPABLE_POLICY: UnmappablePolicy = UnmappablePolicy::Error;
24const DEFAULT_THREAD_COUNT: usize = 1;
25const DEFAULT_ZONED_ENCODING: ZonedEncodingFormat = ZonedEncodingFormat::Auto;
26const DEFAULT_FLOAT_FORMAT: FloatFormat = FloatFormat::IeeeBigEndian;
27
28macro_rules! impl_common_option_builders {
29 () => {
30 #[must_use]
32 #[inline]
33 pub fn with_format(mut self, format: RecordFormat) -> Self {
34 self.format = format;
35 self
36 }
37
38 #[must_use]
40 #[inline]
41 pub fn with_codepage(mut self, codepage: Codepage) -> Self {
42 self.codepage = codepage;
43 self
44 }
45
46 #[must_use]
48 #[inline]
49 pub fn with_strict_mode(mut self, strict_mode: bool) -> Self {
50 self.strict_mode = strict_mode;
51 self
52 }
53
54 #[must_use]
56 #[inline]
57 pub fn with_max_errors(mut self, max_errors: Option<u64>) -> Self {
58 self.max_errors = max_errors;
59 self
60 }
61
62 #[must_use]
64 #[inline]
65 pub fn with_threads(mut self, threads: usize) -> Self {
66 self.threads = threads;
67 self
68 }
69
70 #[must_use]
72 #[inline]
73 pub fn with_json_number_mode(mut self, mode: JsonNumberMode) -> Self {
74 self.json_number_mode = mode;
75 self
76 }
77
78 #[must_use]
80 #[inline]
81 pub fn with_preferred_zoned_encoding(
82 mut self,
83 preferred_zoned_encoding: ZonedEncodingFormat,
84 ) -> Self {
85 self.preferred_zoned_encoding = preferred_zoned_encoding;
86 self
87 }
88
89 #[must_use]
91 #[inline]
92 pub fn with_float_format(mut self, float_format: FloatFormat) -> Self {
93 self.float_format = float_format;
94 self
95 }
96 };
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum, Default)]
114pub enum FloatFormat {
115 #[default]
117 #[value(name = "ieee-be", alias = "ieee", alias = "ieee-big-endian")]
118 IeeeBigEndian,
119 #[value(name = "ibm-hex", alias = "ibm")]
121 IbmHex,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126#[allow(clippy::struct_excessive_bools)] pub struct DecodeOptions {
128 pub format: RecordFormat,
130 pub codepage: Codepage,
132 pub json_number_mode: JsonNumberMode,
134 pub emit_filler: bool,
136 pub emit_meta: bool,
138 pub emit_raw: RawMode,
140 pub strict_mode: bool,
142 pub max_errors: Option<u64>,
144 pub on_decode_unmappable: UnmappablePolicy,
146 pub threads: usize,
148 pub preserve_zoned_encoding: bool,
154 pub preferred_zoned_encoding: ZonedEncodingFormat,
159 #[serde(default)]
161 pub float_format: FloatFormat,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166#[allow(clippy::struct_excessive_bools)]
167pub struct EncodeOptions {
168 pub format: RecordFormat,
170 pub codepage: Codepage,
172 pub preferred_zoned_encoding: ZonedEncodingFormat,
174 pub use_raw: bool,
176 pub bwz_encode: bool,
178 pub strict_mode: bool,
180 pub max_errors: Option<u64>,
182 pub threads: usize,
184 pub coerce_numbers: bool,
186 pub on_encode_unmappable: UnmappablePolicy,
188 pub json_number_mode: JsonNumberMode,
190 pub zoned_encoding_override: Option<ZonedEncodingFormat>,
199 #[serde(default)]
201 pub float_format: FloatFormat,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
220pub enum RecordFormat {
221 Fixed,
223 RDW,
225}
226
227impl RecordFormat {
228 #[must_use]
230 pub const fn is_fixed(self) -> bool {
231 matches!(self, Self::Fixed)
232 }
233
234 #[must_use]
236 pub const fn is_variable(self) -> bool {
237 matches!(self, Self::RDW)
238 }
239
240 #[must_use]
242 pub const fn description(self) -> &'static str {
243 match self {
244 Self::Fixed => "Fixed-length records",
245 Self::RDW => "Variable-length records with Record Descriptor Word",
246 }
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
266pub enum JsonNumberMode {
267 Lossless,
269 Native,
271}
272
273impl JsonNumberMode {
274 #[must_use]
276 #[inline]
277 pub const fn is_lossless(self) -> bool {
278 matches!(self, Self::Lossless)
279 }
280
281 #[must_use]
283 #[inline]
284 pub const fn is_native(self) -> bool {
285 matches!(self, Self::Native)
286 }
287
288 #[must_use]
290 #[inline]
291 pub const fn description(self) -> &'static str {
292 match self {
293 Self::Lossless => "Lossless string representation for decimals",
294 Self::Native => "Native JSON numbers where possible",
295 }
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
319pub enum RawMode {
320 Off,
322 Record,
324 Field,
326 #[value(name = "record+rdw")]
328 RecordRDW,
329}
330
331impl Default for DecodeOptions {
332 fn default() -> Self {
333 Self {
334 format: DEFAULT_RECORD_FORMAT,
335 codepage: DEFAULT_CODEPAGE,
336 json_number_mode: DEFAULT_JSON_NUMBER_MODE,
337 emit_filler: false,
338 emit_meta: false,
339 emit_raw: RawMode::Off,
340 strict_mode: false,
341 max_errors: None,
342 on_decode_unmappable: DEFAULT_UNMAPPABLE_POLICY,
343 threads: DEFAULT_THREAD_COUNT,
344 preserve_zoned_encoding: false,
345 preferred_zoned_encoding: DEFAULT_ZONED_ENCODING,
346 float_format: DEFAULT_FLOAT_FORMAT,
347 }
348 }
349}
350
351impl DecodeOptions {
352 #[must_use]
378 #[inline]
379 pub fn new() -> Self {
380 Self::default()
381 }
382
383 impl_common_option_builders!();
384
385 #[must_use]
387 #[inline]
388 pub fn with_emit_filler(mut self, emit_filler: bool) -> Self {
389 self.emit_filler = emit_filler;
390 self
391 }
392
393 #[must_use]
395 #[inline]
396 pub fn with_emit_meta(mut self, emit_meta: bool) -> Self {
397 self.emit_meta = emit_meta;
398 self
399 }
400
401 #[must_use]
409 #[inline]
410 pub fn with_emit_raw(mut self, emit_raw: RawMode) -> Self {
411 self.emit_raw = emit_raw;
412 self
413 }
414
415 #[must_use]
417 #[inline]
418 pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
419 self.on_decode_unmappable = policy;
420 self
421 }
422
423 #[must_use]
431 #[inline]
432 pub fn with_preserve_zoned_encoding(mut self, preserve_zoned_encoding: bool) -> Self {
433 self.preserve_zoned_encoding = preserve_zoned_encoding;
434 self
435 }
436}
437
438impl Default for EncodeOptions {
439 fn default() -> Self {
440 Self {
441 format: DEFAULT_RECORD_FORMAT,
442 codepage: DEFAULT_CODEPAGE,
443 preferred_zoned_encoding: DEFAULT_ZONED_ENCODING,
444 use_raw: false,
445 bwz_encode: false,
446 strict_mode: false,
447 max_errors: None,
448 threads: DEFAULT_THREAD_COUNT,
449 coerce_numbers: false,
450 on_encode_unmappable: DEFAULT_UNMAPPABLE_POLICY,
451 json_number_mode: DEFAULT_JSON_NUMBER_MODE,
452 zoned_encoding_override: None,
453 float_format: DEFAULT_FLOAT_FORMAT,
454 }
455 }
456}
457
458impl EncodeOptions {
459 #[must_use]
486 #[inline]
487 pub fn new() -> Self {
488 Self::default()
489 }
490
491 impl_common_option_builders!();
492
493 #[must_use]
495 #[inline]
496 pub fn with_use_raw(mut self, use_raw: bool) -> Self {
497 self.use_raw = use_raw;
498 self
499 }
500
501 #[must_use]
503 #[inline]
504 pub fn with_bwz_encode(mut self, bwz_encode: bool) -> Self {
505 self.bwz_encode = bwz_encode;
506 self
507 }
508
509 #[must_use]
511 #[inline]
512 pub fn with_coerce_numbers(mut self, coerce_numbers: bool) -> Self {
513 self.coerce_numbers = coerce_numbers;
514 self
515 }
516
517 #[must_use]
519 #[inline]
520 pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
521 self.on_encode_unmappable = policy;
522 self
523 }
524
525 #[must_use]
531 #[inline]
532 pub fn with_zoned_encoding_override(
533 mut self,
534 zoned_encoding_override: Option<ZonedEncodingFormat>,
535 ) -> Self {
536 self.zoned_encoding_override = zoned_encoding_override;
537 self
538 }
539
540 #[must_use]
544 #[inline]
545 pub fn with_zoned_encoding_format(mut self, format: ZonedEncodingFormat) -> Self {
546 self.zoned_encoding_override = Some(format);
547 self
548 }
549}
550impl fmt::Display for RecordFormat {
551 #[inline]
552 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553 match self {
554 Self::Fixed => write!(f, "fixed"),
555 Self::RDW => write!(f, "rdw"),
556 }
557 }
558}
559
560impl fmt::Display for JsonNumberMode {
561 #[inline]
562 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
563 match self {
564 Self::Lossless => write!(f, "lossless"),
565 Self::Native => write!(f, "native"),
566 }
567 }
568}
569
570impl fmt::Display for RawMode {
571 #[inline]
572 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573 match self {
574 Self::Off => write!(f, "off"),
575 Self::Record => write!(f, "record"),
576 Self::Field => write!(f, "field"),
577 Self::RecordRDW => write!(f, "record+rdw"),
578 }
579 }
580}
581
582impl fmt::Display for FloatFormat {
583 #[inline]
584 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
585 match self {
586 Self::IeeeBigEndian => write!(f, "ieee-be"),
587 Self::IbmHex => write!(f, "ibm-hex"),
588 }
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 #[test]
597 fn test_zoned_encoding_format_is_ascii() {
598 assert!(ZonedEncodingFormat::Ascii.is_ascii());
599 assert!(!ZonedEncodingFormat::Ebcdic.is_ascii());
600 assert!(!ZonedEncodingFormat::Auto.is_ascii());
601 }
602
603 #[test]
604 fn test_zoned_encoding_format_is_ebcdic() {
605 assert!(!ZonedEncodingFormat::Ascii.is_ebcdic());
606 assert!(ZonedEncodingFormat::Ebcdic.is_ebcdic());
607 assert!(!ZonedEncodingFormat::Auto.is_ebcdic());
608 }
609
610 #[test]
611 fn test_zoned_encoding_format_is_auto() {
612 assert!(!ZonedEncodingFormat::Ascii.is_auto());
613 assert!(!ZonedEncodingFormat::Ebcdic.is_auto());
614 assert!(ZonedEncodingFormat::Auto.is_auto());
615 }
616
617 #[test]
618 fn test_zoned_encoding_format_description() {
619 assert_eq!(
620 ZonedEncodingFormat::Ascii.description(),
621 "ASCII digit zones (0x30-0x39)"
622 );
623 assert_eq!(
624 ZonedEncodingFormat::Ebcdic.description(),
625 "EBCDIC digit zones (0xF0-0xF9)"
626 );
627 assert_eq!(
628 ZonedEncodingFormat::Auto.description(),
629 "Automatic detection based on zone nibbles"
630 );
631 }
632
633 #[test]
634 fn test_zoned_encoding_format_detect_from_byte() {
635 assert_eq!(
637 ZonedEncodingFormat::detect_from_byte(0x35),
638 Some(ZonedEncodingFormat::Ascii)
639 );
640 assert_eq!(
641 ZonedEncodingFormat::detect_from_byte(0x30),
642 Some(ZonedEncodingFormat::Ascii)
643 );
644 assert_eq!(
645 ZonedEncodingFormat::detect_from_byte(0x39),
646 Some(ZonedEncodingFormat::Ascii)
647 );
648
649 assert_eq!(
651 ZonedEncodingFormat::detect_from_byte(0xF5),
652 Some(ZonedEncodingFormat::Ebcdic)
653 );
654 assert_eq!(
655 ZonedEncodingFormat::detect_from_byte(0xF0),
656 Some(ZonedEncodingFormat::Ebcdic)
657 );
658 assert_eq!(
659 ZonedEncodingFormat::detect_from_byte(0xF9),
660 Some(ZonedEncodingFormat::Ebcdic)
661 );
662
663 assert_eq!(ZonedEncodingFormat::detect_from_byte(0x00), None);
665 assert_eq!(ZonedEncodingFormat::detect_from_byte(0x50), None);
666 assert_eq!(
668 ZonedEncodingFormat::detect_from_byte(0xFF),
669 Some(ZonedEncodingFormat::Ebcdic)
670 );
671 }
672
673 #[test]
674 fn test_zoned_encoding_format_display() {
675 assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
676 assert_eq!(format!("{}", ZonedEncodingFormat::Ebcdic), "ebcdic");
677 assert_eq!(format!("{}", ZonedEncodingFormat::Auto), "auto");
678 }
679
680 #[test]
681 fn test_decode_options_default() {
682 let options = DecodeOptions::default();
683 assert_eq!(options.format, RecordFormat::Fixed);
684 assert_eq!(options.codepage, Codepage::CP037);
685 assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
686 assert!(!options.emit_filler);
687 assert!(!options.emit_meta);
688 assert_eq!(options.emit_raw, RawMode::Off);
689 assert!(!options.strict_mode);
690 assert!(options.max_errors.is_none());
691 assert_eq!(options.on_decode_unmappable, UnmappablePolicy::Error);
692 assert_eq!(options.threads, 1);
693 assert!(!options.preserve_zoned_encoding);
694 assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
695 assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
696 }
697
698 #[test]
699 fn test_encode_options_default() {
700 let options = EncodeOptions::default();
701 assert_eq!(options.format, RecordFormat::Fixed);
702 assert_eq!(options.codepage, Codepage::CP037);
703 assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
704 assert!(!options.use_raw);
705 assert!(!options.bwz_encode);
706 assert!(!options.strict_mode);
707 assert_eq!(options.on_encode_unmappable, UnmappablePolicy::Error);
708 assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
709 assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
710 }
711
712 #[test]
713 fn test_record_format_display() {
714 assert_eq!(format!("{}", RecordFormat::Fixed), "fixed");
715 assert_eq!(format!("{}", RecordFormat::RDW), "rdw");
716 }
717
718 #[test]
719 fn test_codepage_display() {
720 assert_eq!(format!("{}", Codepage::CP037), "cp037");
721 assert_eq!(format!("{}", Codepage::CP273), "cp273");
722 assert_eq!(format!("{}", Codepage::CP500), "cp500");
723 assert_eq!(format!("{}", Codepage::CP1047), "cp1047");
724 assert_eq!(format!("{}", Codepage::CP1140), "cp1140");
725 }
726
727 #[test]
728 fn test_json_number_mode_display() {
729 assert_eq!(format!("{}", JsonNumberMode::Lossless), "lossless");
730 assert_eq!(format!("{}", JsonNumberMode::Native), "native");
731 }
732
733 #[test]
734 fn test_raw_mode_display() {
735 assert_eq!(format!("{}", RawMode::Off), "off");
736 assert_eq!(format!("{}", RawMode::Record), "record");
737 assert_eq!(format!("{}", RawMode::Field), "field");
738 assert_eq!(format!("{}", RawMode::RecordRDW), "record+rdw");
739 }
740
741 #[test]
742 fn test_unmappable_policy_display() {
743 assert_eq!(format!("{}", UnmappablePolicy::Error), "error");
744 assert_eq!(format!("{}", UnmappablePolicy::Replace), "replace");
745 assert_eq!(format!("{}", UnmappablePolicy::Skip), "skip");
746 }
747
748 #[test]
749 fn test_decode_options_serialization() {
750 let options = DecodeOptions {
751 format: DEFAULT_RECORD_FORMAT,
752 codepage: DEFAULT_CODEPAGE,
753 json_number_mode: DEFAULT_JSON_NUMBER_MODE,
754 emit_filler: true,
755 emit_meta: true,
756 emit_raw: RawMode::Record,
757 strict_mode: true,
758 max_errors: Some(100),
759 on_decode_unmappable: UnmappablePolicy::Replace,
760 threads: 4,
761 preserve_zoned_encoding: true,
762 preferred_zoned_encoding: ZonedEncodingFormat::Ebcdic,
763 float_format: FloatFormat::IbmHex,
764 };
765
766 let serialized = serde_json::to_string(&options).unwrap();
767 let deserialized: DecodeOptions = serde_json::from_str(&serialized).unwrap();
768
769 assert_eq!(deserialized.format, RecordFormat::Fixed);
770 assert_eq!(deserialized.codepage, Codepage::CP037);
771 assert!(deserialized.emit_filler);
772 assert!(deserialized.emit_meta);
773 assert_eq!(deserialized.emit_raw, RawMode::Record);
774 assert!(deserialized.strict_mode);
775 assert_eq!(deserialized.max_errors, Some(100));
776 assert_eq!(deserialized.on_decode_unmappable, UnmappablePolicy::Replace);
777 assert_eq!(deserialized.threads, 4);
778 assert!(deserialized.preserve_zoned_encoding);
779 assert_eq!(
780 deserialized.preferred_zoned_encoding,
781 ZonedEncodingFormat::Ebcdic
782 );
783 assert_eq!(deserialized.float_format, FloatFormat::IbmHex);
784 }
785
786 #[test]
787 fn test_decode_options_deserialize_missing_float_format_defaults() {
788 let options = DecodeOptions::default();
789 let mut value = serde_json::to_value(options).unwrap();
790 value.as_object_mut().unwrap().remove("float_format");
791 let deserialized: DecodeOptions = serde_json::from_value(value).unwrap();
792 assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
793 }
794
795 #[test]
796 fn test_encode_options_deserialize_missing_float_format_defaults() {
797 let options = EncodeOptions::default();
798 let mut value = serde_json::to_value(options).unwrap();
799 value.as_object_mut().unwrap().remove("float_format");
800 let deserialized: EncodeOptions = serde_json::from_value(value).unwrap();
801 assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
802 }
803}