1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2#![allow(clippy::missing_inline_in_public_items)]
10
11mod parse_error;
12pub use parse_error::{CodecOptionKind, ParseCodecOptionError};
13
14pub use crate::numeric::ZonedEncodingFormat;
17pub use copybook_charset::Codepage;
19pub use copybook_charset::UnmappablePolicy;
21use serde::{Deserialize, Serialize};
22use std::{fmt, str::FromStr};
23
24const DEFAULT_RECORD_FORMAT: RecordFormat = RecordFormat::Fixed;
25const DEFAULT_CODEPAGE: Codepage = Codepage::CP037;
26const DEFAULT_JSON_NUMBER_MODE: JsonNumberMode = JsonNumberMode::Lossless;
27const DEFAULT_UNMAPPABLE_POLICY: UnmappablePolicy = UnmappablePolicy::Error;
28const DEFAULT_THREAD_COUNT: usize = 1;
29const DEFAULT_ZONED_ENCODING: ZonedEncodingFormat = ZonedEncodingFormat::Auto;
30const DEFAULT_FLOAT_FORMAT: FloatFormat = FloatFormat::IeeeBigEndian;
31
32macro_rules! impl_common_option_builders {
33 () => {
34 #[must_use]
36 #[inline]
37 pub fn with_format(mut self, format: RecordFormat) -> Self {
38 self.format = format;
39 self
40 }
41
42 #[must_use]
44 #[inline]
45 pub fn with_codepage(mut self, codepage: Codepage) -> Self {
46 self.codepage = codepage;
47 self
48 }
49
50 #[must_use]
52 #[inline]
53 pub fn with_strict_mode(mut self, strict_mode: bool) -> Self {
54 self.strict_mode = strict_mode;
55 self
56 }
57
58 #[must_use]
60 #[inline]
61 pub fn with_max_errors(mut self, max_errors: Option<u64>) -> Self {
62 self.max_errors = max_errors;
63 self
64 }
65
66 #[must_use]
68 #[inline]
69 pub fn with_threads(mut self, threads: usize) -> Self {
70 self.threads = threads;
71 self
72 }
73
74 #[must_use]
76 #[inline]
77 pub fn with_json_number_mode(mut self, mode: JsonNumberMode) -> Self {
78 self.json_number_mode = mode;
79 self
80 }
81
82 #[must_use]
84 #[inline]
85 pub fn with_preferred_zoned_encoding(
86 mut self,
87 preferred_zoned_encoding: ZonedEncodingFormat,
88 ) -> Self {
89 self.preferred_zoned_encoding = preferred_zoned_encoding;
90 self
91 }
92
93 #[must_use]
95 #[inline]
96 pub fn with_float_format(mut self, float_format: FloatFormat) -> Self {
97 self.float_format = float_format;
98 self
99 }
100 };
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
118pub enum FloatFormat {
119 #[default]
121 IeeeBigEndian,
122 IbmHex,
124}
125
126impl FromStr for FloatFormat {
127 type Err = ParseCodecOptionError;
128
129 fn from_str(input: &str) -> Result<Self, Self::Err> {
130 match input.to_ascii_lowercase().as_str() {
131 "ieee-be" | "ieee" | "ieee-big-endian" => Ok(Self::IeeeBigEndian),
132 "ibm-hex" | "ibm" => Ok(Self::IbmHex),
133 _ => Err(ParseCodecOptionError::new(
134 CodecOptionKind::FloatFormat,
135 input,
136 )),
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143#[allow(clippy::struct_excessive_bools)] pub struct DecodeOptions {
145 pub format: RecordFormat,
147 pub codepage: Codepage,
149 pub json_number_mode: JsonNumberMode,
151 pub emit_filler: bool,
153 pub emit_meta: bool,
155 pub emit_raw: RawMode,
157 pub strict_mode: bool,
159 pub max_errors: Option<u64>,
161 pub on_decode_unmappable: UnmappablePolicy,
163 pub threads: usize,
165 pub preserve_zoned_encoding: bool,
171 pub preferred_zoned_encoding: ZonedEncodingFormat,
176 #[serde(default)]
178 pub float_format: FloatFormat,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183#[allow(clippy::struct_excessive_bools)]
184pub struct EncodeOptions {
185 pub format: RecordFormat,
187 pub codepage: Codepage,
189 pub preferred_zoned_encoding: ZonedEncodingFormat,
191 pub use_raw: bool,
193 pub bwz_encode: bool,
195 pub strict_mode: bool,
197 pub max_errors: Option<u64>,
199 pub threads: usize,
201 pub coerce_numbers: bool,
203 pub on_encode_unmappable: UnmappablePolicy,
205 pub json_number_mode: JsonNumberMode,
207 pub zoned_encoding_override: Option<ZonedEncodingFormat>,
216 #[serde(default)]
218 pub float_format: FloatFormat,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237pub enum RecordFormat {
238 Fixed,
240 RDW,
242 Vb,
244}
245
246impl FromStr for RecordFormat {
247 type Err = ParseCodecOptionError;
248
249 fn from_str(input: &str) -> Result<Self, Self::Err> {
250 match input.to_ascii_lowercase().as_str() {
251 "fixed" => Ok(Self::Fixed),
252 "rdw" => Ok(Self::RDW),
253 "vb" => Ok(Self::Vb),
254 _ => Err(ParseCodecOptionError::new(
255 CodecOptionKind::RecordFormat,
256 input,
257 )),
258 }
259 }
260}
261
262impl RecordFormat {
263 #[must_use]
265 pub const fn is_fixed(self) -> bool {
266 matches!(self, Self::Fixed)
267 }
268
269 #[must_use]
271 pub const fn is_variable(self) -> bool {
272 matches!(self, Self::RDW | Self::Vb)
273 }
274
275 #[must_use]
277 pub const fn description(self) -> &'static str {
278 match self {
279 Self::Fixed => "Fixed-length records",
280 Self::RDW => "Variable-length records with Record Descriptor Word",
281 Self::Vb => "Variable-blocked records with BDW blocks (beta)",
282 }
283 }
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
302pub enum JsonNumberMode {
303 Lossless,
305 Native,
307}
308
309impl FromStr for JsonNumberMode {
310 type Err = ParseCodecOptionError;
311
312 fn from_str(input: &str) -> Result<Self, Self::Err> {
313 match input.to_ascii_lowercase().as_str() {
314 "lossless" => Ok(Self::Lossless),
315 "native" => Ok(Self::Native),
316 _ => Err(ParseCodecOptionError::new(
317 CodecOptionKind::JsonNumberMode,
318 input,
319 )),
320 }
321 }
322}
323
324impl JsonNumberMode {
325 #[must_use]
327 #[inline]
328 pub const fn is_lossless(self) -> bool {
329 matches!(self, Self::Lossless)
330 }
331
332 #[must_use]
334 #[inline]
335 pub const fn is_native(self) -> bool {
336 matches!(self, Self::Native)
337 }
338
339 #[must_use]
341 #[inline]
342 pub const fn description(self) -> &'static str {
343 match self {
344 Self::Lossless => "Lossless string representation for decimals",
345 Self::Native => "Native JSON numbers where possible",
346 }
347 }
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
373pub enum RawMode {
374 Off,
376 Record,
378 Field,
380 RecordRDW,
382}
383
384impl FromStr for RawMode {
385 type Err = ParseCodecOptionError;
386
387 fn from_str(input: &str) -> Result<Self, Self::Err> {
388 match input.to_ascii_lowercase().as_str() {
389 "off" => Ok(Self::Off),
390 "record" => Ok(Self::Record),
391 "field" => Ok(Self::Field),
392 "record+rdw" => Ok(Self::RecordRDW),
393 _ => Err(ParseCodecOptionError::new(CodecOptionKind::RawMode, input)),
394 }
395 }
396}
397
398impl Default for DecodeOptions {
399 fn default() -> Self {
400 Self {
401 format: DEFAULT_RECORD_FORMAT,
402 codepage: DEFAULT_CODEPAGE,
403 json_number_mode: DEFAULT_JSON_NUMBER_MODE,
404 emit_filler: false,
405 emit_meta: false,
406 emit_raw: RawMode::Off,
407 strict_mode: false,
408 max_errors: None,
409 on_decode_unmappable: DEFAULT_UNMAPPABLE_POLICY,
410 threads: DEFAULT_THREAD_COUNT,
411 preserve_zoned_encoding: false,
412 preferred_zoned_encoding: DEFAULT_ZONED_ENCODING,
413 float_format: DEFAULT_FLOAT_FORMAT,
414 }
415 }
416}
417
418impl DecodeOptions {
419 #[must_use]
445 #[inline]
446 pub fn new() -> Self {
447 Self::default()
448 }
449
450 impl_common_option_builders!();
451
452 #[must_use]
454 #[inline]
455 pub fn with_emit_filler(mut self, emit_filler: bool) -> Self {
456 self.emit_filler = emit_filler;
457 self
458 }
459
460 #[must_use]
462 #[inline]
463 pub fn with_emit_meta(mut self, emit_meta: bool) -> Self {
464 self.emit_meta = emit_meta;
465 self
466 }
467
468 #[must_use]
478 #[inline]
479 pub fn with_emit_raw(mut self, emit_raw: RawMode) -> Self {
480 self.emit_raw = emit_raw;
481 self
482 }
483
484 #[must_use]
486 #[inline]
487 pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
488 self.on_decode_unmappable = policy;
489 self
490 }
491
492 #[must_use]
500 #[inline]
501 pub fn with_preserve_zoned_encoding(mut self, preserve_zoned_encoding: bool) -> Self {
502 self.preserve_zoned_encoding = preserve_zoned_encoding;
503 self
504 }
505}
506
507impl Default for EncodeOptions {
508 fn default() -> Self {
509 Self {
510 format: DEFAULT_RECORD_FORMAT,
511 codepage: DEFAULT_CODEPAGE,
512 preferred_zoned_encoding: DEFAULT_ZONED_ENCODING,
513 use_raw: false,
514 bwz_encode: false,
515 strict_mode: false,
516 max_errors: None,
517 threads: DEFAULT_THREAD_COUNT,
518 coerce_numbers: false,
519 on_encode_unmappable: DEFAULT_UNMAPPABLE_POLICY,
520 json_number_mode: DEFAULT_JSON_NUMBER_MODE,
521 zoned_encoding_override: None,
522 float_format: DEFAULT_FLOAT_FORMAT,
523 }
524 }
525}
526
527impl EncodeOptions {
528 #[must_use]
555 #[inline]
556 pub fn new() -> Self {
557 Self::default()
558 }
559
560 impl_common_option_builders!();
561
562 #[must_use]
564 #[inline]
565 pub fn with_use_raw(mut self, use_raw: bool) -> Self {
566 self.use_raw = use_raw;
567 self
568 }
569
570 #[must_use]
572 #[inline]
573 pub fn with_bwz_encode(mut self, bwz_encode: bool) -> Self {
574 self.bwz_encode = bwz_encode;
575 self
576 }
577
578 #[must_use]
580 #[inline]
581 pub fn with_coerce_numbers(mut self, coerce_numbers: bool) -> Self {
582 self.coerce_numbers = coerce_numbers;
583 self
584 }
585
586 #[must_use]
588 #[inline]
589 pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
590 self.on_encode_unmappable = policy;
591 self
592 }
593
594 #[must_use]
600 #[inline]
601 pub fn with_zoned_encoding_override(
602 mut self,
603 zoned_encoding_override: Option<ZonedEncodingFormat>,
604 ) -> Self {
605 self.zoned_encoding_override = zoned_encoding_override;
606 self
607 }
608
609 #[must_use]
613 #[inline]
614 pub fn with_zoned_encoding_format(mut self, format: ZonedEncodingFormat) -> Self {
615 self.zoned_encoding_override = Some(format);
616 self
617 }
618}
619impl fmt::Display for RecordFormat {
620 #[inline]
621 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
622 match self {
623 Self::Fixed => write!(f, "fixed"),
624 Self::RDW => write!(f, "rdw"),
625 Self::Vb => write!(f, "vb"),
626 }
627 }
628}
629
630impl fmt::Display for JsonNumberMode {
631 #[inline]
632 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
633 match self {
634 Self::Lossless => write!(f, "lossless"),
635 Self::Native => write!(f, "native"),
636 }
637 }
638}
639
640impl fmt::Display for RawMode {
641 #[inline]
642 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
643 match self {
644 Self::Off => write!(f, "off"),
645 Self::Record => write!(f, "record"),
646 Self::Field => write!(f, "field"),
647 Self::RecordRDW => write!(f, "record+rdw"),
648 }
649 }
650}
651
652impl fmt::Display for FloatFormat {
653 #[inline]
654 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655 match self {
656 Self::IeeeBigEndian => write!(f, "ieee-be"),
657 Self::IbmHex => write!(f, "ibm-hex"),
658 }
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
667 fn test_zoned_encoding_format_is_ascii() {
668 assert!(ZonedEncodingFormat::Ascii.is_ascii());
669 assert!(!ZonedEncodingFormat::Ebcdic.is_ascii());
670 assert!(!ZonedEncodingFormat::Auto.is_ascii());
671 }
672
673 #[test]
674 fn test_zoned_encoding_format_is_ebcdic() {
675 assert!(!ZonedEncodingFormat::Ascii.is_ebcdic());
676 assert!(ZonedEncodingFormat::Ebcdic.is_ebcdic());
677 assert!(!ZonedEncodingFormat::Auto.is_ebcdic());
678 }
679
680 #[test]
681 fn test_zoned_encoding_format_is_auto() {
682 assert!(!ZonedEncodingFormat::Ascii.is_auto());
683 assert!(!ZonedEncodingFormat::Ebcdic.is_auto());
684 assert!(ZonedEncodingFormat::Auto.is_auto());
685 }
686
687 #[test]
688 fn test_zoned_encoding_format_description() {
689 assert_eq!(
690 ZonedEncodingFormat::Ascii.description(),
691 "ASCII digit zones (0x30-0x39)"
692 );
693 assert_eq!(
694 ZonedEncodingFormat::Ebcdic.description(),
695 "EBCDIC digit zones (0xF0-0xF9)"
696 );
697 assert_eq!(
698 ZonedEncodingFormat::Auto.description(),
699 "Automatic detection based on zone nibbles"
700 );
701 }
702
703 #[test]
704 fn test_zoned_encoding_format_detect_from_byte() {
705 assert_eq!(
707 ZonedEncodingFormat::detect_from_byte(0x35),
708 Some(ZonedEncodingFormat::Ascii)
709 );
710 assert_eq!(
711 ZonedEncodingFormat::detect_from_byte(0x30),
712 Some(ZonedEncodingFormat::Ascii)
713 );
714 assert_eq!(
715 ZonedEncodingFormat::detect_from_byte(0x39),
716 Some(ZonedEncodingFormat::Ascii)
717 );
718
719 assert_eq!(
721 ZonedEncodingFormat::detect_from_byte(0xF5),
722 Some(ZonedEncodingFormat::Ebcdic)
723 );
724 assert_eq!(
725 ZonedEncodingFormat::detect_from_byte(0xF0),
726 Some(ZonedEncodingFormat::Ebcdic)
727 );
728 assert_eq!(
729 ZonedEncodingFormat::detect_from_byte(0xF9),
730 Some(ZonedEncodingFormat::Ebcdic)
731 );
732
733 assert_eq!(ZonedEncodingFormat::detect_from_byte(0x00), None);
735 assert_eq!(ZonedEncodingFormat::detect_from_byte(0x50), None);
736 assert_eq!(
738 ZonedEncodingFormat::detect_from_byte(0xFF),
739 Some(ZonedEncodingFormat::Ebcdic)
740 );
741 }
742
743 #[test]
744 fn test_zoned_encoding_format_display() {
745 assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
746 assert_eq!(format!("{}", ZonedEncodingFormat::Ebcdic), "ebcdic");
747 assert_eq!(format!("{}", ZonedEncodingFormat::Auto), "auto");
748 }
749
750 #[test]
751 fn test_decode_options_default() {
752 let options = DecodeOptions::default();
753 assert_eq!(options.format, RecordFormat::Fixed);
754 assert_eq!(options.codepage, Codepage::CP037);
755 assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
756 assert!(!options.emit_filler);
757 assert!(!options.emit_meta);
758 assert_eq!(options.emit_raw, RawMode::Off);
759 assert!(!options.strict_mode);
760 assert!(options.max_errors.is_none());
761 assert_eq!(options.on_decode_unmappable, UnmappablePolicy::Error);
762 assert_eq!(options.threads, 1);
763 assert!(!options.preserve_zoned_encoding);
764 assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
765 assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
766 }
767
768 #[test]
769 fn test_encode_options_default() {
770 let options = EncodeOptions::default();
771 assert_eq!(options.format, RecordFormat::Fixed);
772 assert_eq!(options.codepage, Codepage::CP037);
773 assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
774 assert!(!options.use_raw);
775 assert!(!options.bwz_encode);
776 assert!(!options.strict_mode);
777 assert_eq!(options.on_encode_unmappable, UnmappablePolicy::Error);
778 assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
779 assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
780 }
781
782 #[test]
783 fn test_record_format_display() {
784 assert_eq!(format!("{}", RecordFormat::Fixed), "fixed");
785 assert_eq!(format!("{}", RecordFormat::RDW), "rdw");
786 }
787
788 #[test]
789 fn test_codepage_display() {
790 assert_eq!(format!("{}", Codepage::CP037), "cp037");
791 assert_eq!(format!("{}", Codepage::CP273), "cp273");
792 assert_eq!(format!("{}", Codepage::CP500), "cp500");
793 assert_eq!(format!("{}", Codepage::CP1047), "cp1047");
794 assert_eq!(format!("{}", Codepage::CP1140), "cp1140");
795 }
796
797 #[test]
798 fn test_json_number_mode_display() {
799 assert_eq!(format!("{}", JsonNumberMode::Lossless), "lossless");
800 assert_eq!(format!("{}", JsonNumberMode::Native), "native");
801 }
802
803 #[test]
804 fn test_raw_mode_display() {
805 assert_eq!(format!("{}", RawMode::Off), "off");
806 assert_eq!(format!("{}", RawMode::Record), "record");
807 assert_eq!(format!("{}", RawMode::Field), "field");
808 assert_eq!(format!("{}", RawMode::RecordRDW), "record+rdw");
809 }
810
811 #[test]
812 fn test_unmappable_policy_display() {
813 assert_eq!(format!("{}", UnmappablePolicy::Error), "error");
814 assert_eq!(format!("{}", UnmappablePolicy::Replace), "replace");
815 assert_eq!(format!("{}", UnmappablePolicy::Skip), "skip");
816 }
817
818 #[test]
819 fn test_decode_options_serialization() {
820 let options = DecodeOptions {
821 format: DEFAULT_RECORD_FORMAT,
822 codepage: DEFAULT_CODEPAGE,
823 json_number_mode: DEFAULT_JSON_NUMBER_MODE,
824 emit_filler: true,
825 emit_meta: true,
826 emit_raw: RawMode::Record,
827 strict_mode: true,
828 max_errors: Some(100),
829 on_decode_unmappable: UnmappablePolicy::Replace,
830 threads: 4,
831 preserve_zoned_encoding: true,
832 preferred_zoned_encoding: ZonedEncodingFormat::Ebcdic,
833 float_format: FloatFormat::IbmHex,
834 };
835
836 let serialized = serde_json::to_string(&options).unwrap();
837 let deserialized: DecodeOptions = serde_json::from_str(&serialized).unwrap();
838
839 assert_eq!(deserialized.format, RecordFormat::Fixed);
840 assert_eq!(deserialized.codepage, Codepage::CP037);
841 assert!(deserialized.emit_filler);
842 assert!(deserialized.emit_meta);
843 assert_eq!(deserialized.emit_raw, RawMode::Record);
844 assert!(deserialized.strict_mode);
845 assert_eq!(deserialized.max_errors, Some(100));
846 assert_eq!(deserialized.on_decode_unmappable, UnmappablePolicy::Replace);
847 assert_eq!(deserialized.threads, 4);
848 assert!(deserialized.preserve_zoned_encoding);
849 assert_eq!(
850 deserialized.preferred_zoned_encoding,
851 ZonedEncodingFormat::Ebcdic
852 );
853 assert_eq!(deserialized.float_format, FloatFormat::IbmHex);
854 }
855
856 #[test]
857 fn test_decode_options_deserialize_missing_float_format_defaults() {
858 let options = DecodeOptions::default();
859 let mut value = serde_json::to_value(options).unwrap();
860 value.as_object_mut().unwrap().remove("float_format");
861 let deserialized: DecodeOptions = serde_json::from_value(value).unwrap();
862 assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
863 }
864
865 #[test]
866 fn test_encode_options_deserialize_missing_float_format_defaults() {
867 let options = EncodeOptions::default();
868 let mut value = serde_json::to_value(options).unwrap();
869 value.as_object_mut().unwrap().remove("float_format");
870 let deserialized: EncodeOptions = serde_json::from_value(value).unwrap();
871 assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
872 }
873}