Skip to main content

copybook_options/
lib.rs

1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Configuration option types shared across copybook-rs crates.
4//!
5//! This crate defines [`DecodeOptions`], [`EncodeOptions`], [`RecordFormat`],
6//! [`JsonNumberMode`], [`RawMode`], and [`FloatFormat`] — the common configuration
7//! surface used by the codec and CLI layers.
8#![allow(clippy::missing_inline_in_public_items)]
9
10// Re-export from copybook-charset for public API
11/// Codepage identifier for EBCDIC/ASCII character encoding.
12pub use copybook_charset::Codepage;
13/// Policy for handling unmappable characters during codepage conversion.
14pub use copybook_charset::UnmappablePolicy;
15/// Zoned decimal encoding format (ASCII, EBCDIC, or auto-detect).
16pub use copybook_zoned_format::ZonedEncodingFormat;
17use serde::{Deserialize, Serialize};
18use std::fmt;
19
20/// Floating-point binary format for COMP-1/COMP-2 fields.
21///
22/// Copybooks define field usage but not the compiler's concrete floating-point
23/// representation. This option makes the decode/encode interpretation explicit.
24///
25/// # Examples
26///
27/// ```
28/// use copybook_options::FloatFormat;
29///
30/// let fmt = FloatFormat::default();
31/// assert_eq!(fmt, FloatFormat::IeeeBigEndian);
32/// assert_eq!(format!("{fmt}"), "ieee-be");
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum, Default)]
35pub enum FloatFormat {
36    /// IEEE-754 big-endian binary format.
37    #[default]
38    #[value(name = "ieee-be", alias = "ieee", alias = "ieee-big-endian")]
39    IeeeBigEndian,
40    /// IBM hexadecimal floating-point format.
41    #[value(name = "ibm-hex", alias = "ibm")]
42    IbmHex,
43}
44
45/// Options for decoding operations
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[allow(clippy::struct_excessive_bools)] // Many boolean options are needed for decode configuration
48pub struct DecodeOptions {
49    /// Record format
50    pub format: RecordFormat,
51    /// Character encoding
52    pub codepage: Codepage,
53    /// JSON number representation
54    pub json_number_mode: JsonNumberMode,
55    /// Whether to emit FILLER fields
56    pub emit_filler: bool,
57    /// Whether to emit metadata
58    pub emit_meta: bool,
59    /// Raw data capture mode
60    pub emit_raw: RawMode,
61    /// Error handling mode
62    pub strict_mode: bool,
63    /// Maximum errors before stopping
64    pub max_errors: Option<u64>,
65    /// Policy for unmappable characters
66    pub on_decode_unmappable: UnmappablePolicy,
67    /// Number of threads for parallel processing
68    pub threads: usize,
69    /// Enable zoned decimal encoding preservation for binary round-trip consistency
70    ///
71    /// When enabled, the decoder captures the original encoding format (ASCII vs EBCDIC)
72    /// and includes it in metadata for use during re-encoding to maintain byte-level
73    /// fidelity in encode/decode cycles.
74    pub preserve_zoned_encoding: bool,
75    /// Preferred encoding format when auto-detection is ambiguous
76    ///
77    /// Used as fallback when `ZonedEncodingFormat::Auto` cannot determine the format
78    /// from the data (e.g., all-zero fields, mixed encodings).
79    pub preferred_zoned_encoding: ZonedEncodingFormat,
80    /// Floating-point representation for COMP-1/COMP-2 fields.
81    #[serde(default)]
82    pub float_format: FloatFormat,
83}
84
85/// Options for encoding operations
86#[derive(Debug, Clone, Serialize, Deserialize)]
87#[allow(clippy::struct_excessive_bools)]
88pub struct EncodeOptions {
89    /// Record format
90    pub format: RecordFormat,
91    /// Character encoding
92    pub codepage: Codepage,
93    /// Fallback zoned decimal encoding format when no override or metadata applies
94    pub preferred_zoned_encoding: ZonedEncodingFormat,
95    /// Whether to use raw data when available
96    pub use_raw: bool,
97    /// BLANK WHEN ZERO encoding policy
98    pub bwz_encode: bool,
99    /// Error handling mode
100    pub strict_mode: bool,
101    /// Maximum errors before stopping
102    pub max_errors: Option<u64>,
103    /// Number of threads for parallel processing
104    pub threads: usize,
105    /// Whether to coerce non-string JSON numbers to strings before encoding
106    pub coerce_numbers: bool,
107    /// Policy for unmappable characters during encoding
108    pub on_encode_unmappable: UnmappablePolicy,
109    /// JSON number representation mode (used when round-tripping)
110    pub json_number_mode: JsonNumberMode,
111    /// Explicit zoned decimal encoding format override
112    ///
113    /// When specified, forces all zoned decimal fields to use this encoding format,
114    /// overriding any preserved format from decode operations. This provides the
115    /// highest precedence in the format selection hierarchy:
116    /// 1. Explicit override (this field)
117    /// 2. Preserved format from decode metadata
118    /// 3. EBCDIC default for mainframe compatibility
119    pub zoned_encoding_override: Option<ZonedEncodingFormat>,
120    /// Floating-point representation for COMP-1/COMP-2 fields.
121    #[serde(default)]
122    pub float_format: FloatFormat,
123}
124
125/// Record format specification
126///
127/// Controls whether records have a fixed byte length (LRECL) or use
128/// variable-length RDW (Record Descriptor Word) framing.
129///
130/// # Examples
131///
132/// ```
133/// use copybook_options::RecordFormat;
134///
135/// let fmt = RecordFormat::Fixed;
136/// assert!(fmt.is_fixed());
137/// assert!(!fmt.is_variable());
138/// assert_eq!(fmt.description(), "Fixed-length records");
139/// ```
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
141pub enum RecordFormat {
142    /// Fixed-length records
143    Fixed,
144    /// Variable-length records with RDW
145    RDW,
146}
147
148impl RecordFormat {
149    /// Check if this is a fixed-length record format
150    #[must_use]
151    pub const fn is_fixed(self) -> bool {
152        matches!(self, Self::Fixed)
153    }
154
155    /// Check if this is a variable-length record format
156    #[must_use]
157    pub const fn is_variable(self) -> bool {
158        matches!(self, Self::RDW)
159    }
160
161    /// Get a human-readable description of the format
162    #[must_use]
163    pub const fn description(self) -> &'static str {
164        match self {
165            Self::Fixed => "Fixed-length records",
166            Self::RDW => "Variable-length records with Record Descriptor Word",
167        }
168    }
169}
170
171/// JSON number representation mode
172///
173/// Controls how COBOL numeric fields are represented in JSON output.
174/// `Lossless` preserves exact decimal precision as strings; `Native` uses
175/// JSON number types where the value fits without precision loss.
176///
177/// # Examples
178///
179/// ```
180/// use copybook_options::JsonNumberMode;
181///
182/// let mode = JsonNumberMode::Lossless;
183/// assert!(mode.is_lossless());
184/// assert_eq!(mode.description(), "Lossless string representation for decimals");
185/// ```
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
187pub enum JsonNumberMode {
188    /// Lossless string representation for decimals
189    Lossless,
190    /// Native JSON numbers where possible
191    Native,
192}
193
194impl JsonNumberMode {
195    /// Check if this mode uses lossless string representation
196    #[must_use]
197    #[inline]
198    pub const fn is_lossless(self) -> bool {
199        matches!(self, Self::Lossless)
200    }
201
202    /// Check if this mode uses native JSON numbers
203    #[must_use]
204    #[inline]
205    pub const fn is_native(self) -> bool {
206        matches!(self, Self::Native)
207    }
208
209    /// Get a human-readable description of the mode
210    #[must_use]
211    #[inline]
212    pub const fn description(self) -> &'static str {
213        match self {
214            Self::Lossless => "Lossless string representation for decimals",
215            Self::Native => "Native JSON numbers where possible",
216        }
217    }
218}
219
220/// Raw data capture mode
221///
222/// Controls whether and how binary record data is captured in the output:
223/// - `Off` — no raw data (default, lowest overhead)
224/// - `Record` — full record payload in `__raw_b64`
225/// - `RecordRDW` — RDW header + payload in `__raw_b64`
226/// - `Field` — per-field raw bytes in `<FIELD_NAME>__raw_b64`
227///
228/// # Examples
229///
230/// ```
231/// use copybook_options::RawMode;
232///
233/// let mode = RawMode::Off;
234/// assert_eq!(mode, RawMode::Off);
235///
236/// let field_mode = RawMode::Field;
237/// assert_ne!(field_mode, RawMode::Off);
238/// ```
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
240pub enum RawMode {
241    /// No raw data capture
242    Off,
243    /// Capture record-level raw data
244    Record,
245    /// Capture field-level raw data
246    Field,
247    /// Capture record and RDW header
248    #[value(name = "record+rdw")]
249    RecordRDW,
250}
251
252impl Default for DecodeOptions {
253    fn default() -> Self {
254        Self {
255            format: RecordFormat::Fixed,
256            codepage: Codepage::CP037,
257            json_number_mode: JsonNumberMode::Lossless,
258            emit_filler: false,
259            emit_meta: false,
260            emit_raw: RawMode::Off,
261            strict_mode: false,
262            max_errors: None,
263            on_decode_unmappable: UnmappablePolicy::Error,
264            threads: 1,
265            preserve_zoned_encoding: false,
266            preferred_zoned_encoding: ZonedEncodingFormat::Auto,
267            float_format: FloatFormat::IeeeBigEndian,
268        }
269    }
270}
271
272impl DecodeOptions {
273    /// Create new decode options with default values
274    ///
275    /// Returns options configured for:
276    /// - Fixed record format
277    /// - CP037 EBCDIC codepage
278    /// - Lossless JSON number mode
279    /// - Single-threaded processing
280    ///
281    /// Use the builder methods to customize:
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// use copybook_options::{DecodeOptions, Codepage, JsonNumberMode, RecordFormat, RawMode};
287    ///
288    /// let opts = DecodeOptions::new()
289    ///     .with_codepage(Codepage::CP037)
290    ///     .with_format(RecordFormat::Fixed)
291    ///     .with_json_number_mode(JsonNumberMode::Lossless)
292    ///     .with_emit_meta(true)
293    ///     .with_threads(4);
294    ///
295    /// assert_eq!(opts.threads, 4);
296    /// assert!(opts.emit_meta);
297    /// ```
298    #[must_use]
299    #[inline]
300    pub fn new() -> Self {
301        Self::default()
302    }
303
304    /// Set the record format
305    #[must_use]
306    #[inline]
307    pub fn with_format(mut self, format: RecordFormat) -> Self {
308        self.format = format;
309        self
310    }
311
312    /// Set the codepage
313    #[must_use]
314    #[inline]
315    pub fn with_codepage(mut self, codepage: Codepage) -> Self {
316        self.codepage = codepage;
317        self
318    }
319
320    /// Set the JSON number mode
321    #[must_use]
322    #[inline]
323    pub fn with_json_number_mode(mut self, mode: JsonNumberMode) -> Self {
324        self.json_number_mode = mode;
325        self
326    }
327
328    /// Enable or disable FILLER field emission
329    #[must_use]
330    #[inline]
331    pub fn with_emit_filler(mut self, emit_filler: bool) -> Self {
332        self.emit_filler = emit_filler;
333        self
334    }
335
336    /// Enable or disable metadata emission
337    #[must_use]
338    #[inline]
339    pub fn with_emit_meta(mut self, emit_meta: bool) -> Self {
340        self.emit_meta = emit_meta;
341        self
342    }
343
344    /// Set the raw data capture mode
345    ///
346    /// Controls whether and how raw binary data is included in decode output:
347    /// - `RawMode::Off` — no raw data (default)
348    /// - `RawMode::Record` — record payload in `__raw_b64`
349    /// - `RawMode::RecordRDW` — RDW header + payload in `__raw_b64`
350    /// - `RawMode::Field` — per-field raw values in `<FIELD>__raw_b64`
351    #[must_use]
352    #[inline]
353    pub fn with_emit_raw(mut self, emit_raw: RawMode) -> Self {
354        self.emit_raw = emit_raw;
355        self
356    }
357
358    /// Enable or disable strict mode
359    #[must_use]
360    #[inline]
361    pub fn with_strict_mode(mut self, strict_mode: bool) -> Self {
362        self.strict_mode = strict_mode;
363        self
364    }
365
366    /// Set the maximum number of errors before stopping
367    #[must_use]
368    #[inline]
369    pub fn with_max_errors(mut self, max_errors: Option<u64>) -> Self {
370        self.max_errors = max_errors;
371        self
372    }
373
374    /// Set the policy for unmappable characters
375    #[must_use]
376    #[inline]
377    pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
378        self.on_decode_unmappable = policy;
379        self
380    }
381
382    /// Set the number of threads for parallel processing
383    #[must_use]
384    #[inline]
385    pub fn with_threads(mut self, threads: usize) -> Self {
386        self.threads = threads;
387        self
388    }
389
390    // === Zoned Decimal Encoding Configuration ===
391
392    /// Enable zoned decimal encoding preservation for round-trip fidelity
393    ///
394    /// When enabled, the decoder will detect and preserve the original encoding
395    /// format (ASCII vs EBCDIC) for use during subsequent encoding operations.
396    /// This ensures byte-level consistency in encode/decode cycles.
397    #[must_use]
398    #[inline]
399    pub fn with_preserve_zoned_encoding(mut self, preserve_zoned_encoding: bool) -> Self {
400        self.preserve_zoned_encoding = preserve_zoned_encoding;
401        self
402    }
403
404    /// Set the preferred zoned encoding format for ambiguous detection
405    ///
406    /// This format is used as a fallback when auto-detection cannot determine
407    /// the encoding from the data (e.g., all-zero fields, mixed encodings).
408    #[must_use]
409    #[inline]
410    pub fn with_preferred_zoned_encoding(
411        mut self,
412        preferred_zoned_encoding: ZonedEncodingFormat,
413    ) -> Self {
414        self.preferred_zoned_encoding = preferred_zoned_encoding;
415        self
416    }
417
418    /// Set floating-point representation for COMP-1/COMP-2 fields
419    #[must_use]
420    #[inline]
421    pub fn with_float_format(mut self, float_format: FloatFormat) -> Self {
422        self.float_format = float_format;
423        self
424    }
425}
426
427impl Default for EncodeOptions {
428    fn default() -> Self {
429        Self {
430            format: RecordFormat::Fixed,
431            codepage: Codepage::CP037,
432            preferred_zoned_encoding: ZonedEncodingFormat::Auto,
433            use_raw: false,
434            bwz_encode: false,
435            strict_mode: false,
436            max_errors: None,
437            threads: 1,
438            coerce_numbers: false,
439            on_encode_unmappable: UnmappablePolicy::Error,
440            json_number_mode: JsonNumberMode::Lossless,
441            zoned_encoding_override: None,
442            float_format: FloatFormat::IeeeBigEndian,
443        }
444    }
445}
446
447impl EncodeOptions {
448    /// Create new encode options with default values
449    ///
450    /// Returns options configured for:
451    /// - Fixed record format
452    /// - CP037 EBCDIC codepage
453    /// - Single-threaded processing
454    /// - BLANK WHEN ZERO disabled
455    ///
456    /// Use the builder methods to customize:
457    ///
458    /// # Examples
459    ///
460    /// ```
461    /// use copybook_options::{EncodeOptions, Codepage, RecordFormat};
462    ///
463    /// let opts = EncodeOptions::new()
464    ///     .with_codepage(Codepage::CP037)
465    ///     .with_format(RecordFormat::Fixed)
466    ///     .with_bwz_encode(true)
467    ///     .with_coerce_numbers(true)
468    ///     .with_threads(4);
469    ///
470    /// assert_eq!(opts.threads, 4);
471    /// assert!(opts.bwz_encode);
472    /// assert!(opts.coerce_numbers);
473    /// ```
474    #[must_use]
475    #[inline]
476    pub fn new() -> Self {
477        Self::default()
478    }
479
480    /// Set the record format
481    #[must_use]
482    #[inline]
483    pub fn with_format(mut self, format: RecordFormat) -> Self {
484        self.format = format;
485        self
486    }
487
488    /// Set the codepage
489    #[must_use]
490    #[inline]
491    pub fn with_codepage(mut self, codepage: Codepage) -> Self {
492        self.codepage = codepage;
493        self
494    }
495
496    /// Enable or disable raw data usage
497    #[must_use]
498    #[inline]
499    pub fn with_use_raw(mut self, use_raw: bool) -> Self {
500        self.use_raw = use_raw;
501        self
502    }
503
504    /// Enable or disable BLANK WHEN ZERO encoding
505    #[must_use]
506    #[inline]
507    pub fn with_bwz_encode(mut self, bwz_encode: bool) -> Self {
508        self.bwz_encode = bwz_encode;
509        self
510    }
511
512    /// Set the preferred zoned encoding fallback when overrides and preserved formats are absent
513    #[must_use]
514    #[inline]
515    pub fn with_preferred_zoned_encoding(
516        mut self,
517        preferred_zoned_encoding: ZonedEncodingFormat,
518    ) -> Self {
519        self.preferred_zoned_encoding = preferred_zoned_encoding;
520        self
521    }
522
523    /// Enable or disable strict mode
524    #[must_use]
525    #[inline]
526    pub fn with_strict_mode(mut self, strict_mode: bool) -> Self {
527        self.strict_mode = strict_mode;
528        self
529    }
530
531    /// Set the maximum number of errors before stopping
532    #[must_use]
533    #[inline]
534    pub fn with_max_errors(mut self, max_errors: Option<u64>) -> Self {
535        self.max_errors = max_errors;
536        self
537    }
538
539    /// Set the number of threads for parallel processing
540    #[must_use]
541    #[inline]
542    pub fn with_threads(mut self, threads: usize) -> Self {
543        self.threads = threads;
544        self
545    }
546
547    /// Enable or disable number coercion
548    #[must_use]
549    #[inline]
550    pub fn with_coerce_numbers(mut self, coerce_numbers: bool) -> Self {
551        self.coerce_numbers = coerce_numbers;
552        self
553    }
554
555    /// Set the policy for unmappable characters during encoding
556    #[must_use]
557    #[inline]
558    pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
559        self.on_encode_unmappable = policy;
560        self
561    }
562
563    /// Set the JSON number mode
564    #[must_use]
565    #[inline]
566    pub fn with_json_number_mode(mut self, mode: JsonNumberMode) -> Self {
567        self.json_number_mode = mode;
568        self
569    }
570
571    /// Set explicit zoned decimal encoding format override
572    ///
573    /// Forces all zoned decimal fields to use the specified encoding format,
574    /// overriding any preserved format from decode operations. Use `None` to
575    /// disable override and respect preserved formats.
576    #[must_use]
577    #[inline]
578    pub fn with_zoned_encoding_override(
579        mut self,
580        zoned_encoding_override: Option<ZonedEncodingFormat>,
581    ) -> Self {
582        self.zoned_encoding_override = zoned_encoding_override;
583        self
584    }
585
586    /// Convenience method to set explicit zoned encoding format
587    ///
588    /// Equivalent to `with_zoned_encoding_override(Some(format))`.
589    #[must_use]
590    #[inline]
591    pub fn with_zoned_encoding_format(mut self, format: ZonedEncodingFormat) -> Self {
592        self.zoned_encoding_override = Some(format);
593        self
594    }
595
596    /// Set floating-point representation for COMP-1/COMP-2 fields
597    #[must_use]
598    #[inline]
599    pub fn with_float_format(mut self, float_format: FloatFormat) -> Self {
600        self.float_format = float_format;
601        self
602    }
603}
604impl fmt::Display for RecordFormat {
605    #[inline]
606    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
607        match self {
608            Self::Fixed => write!(f, "fixed"),
609            Self::RDW => write!(f, "rdw"),
610        }
611    }
612}
613
614impl fmt::Display for JsonNumberMode {
615    #[inline]
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        match self {
618            Self::Lossless => write!(f, "lossless"),
619            Self::Native => write!(f, "native"),
620        }
621    }
622}
623
624impl fmt::Display for RawMode {
625    #[inline]
626    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
627        match self {
628            Self::Off => write!(f, "off"),
629            Self::Record => write!(f, "record"),
630            Self::Field => write!(f, "field"),
631            Self::RecordRDW => write!(f, "record+rdw"),
632        }
633    }
634}
635
636impl fmt::Display for FloatFormat {
637    #[inline]
638    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
639        match self {
640            Self::IeeeBigEndian => write!(f, "ieee-be"),
641            Self::IbmHex => write!(f, "ibm-hex"),
642        }
643    }
644}
645
646#[cfg(test)]
647mod tests {
648    use super::*;
649
650    #[test]
651    fn test_zoned_encoding_format_is_ascii() {
652        assert!(ZonedEncodingFormat::Ascii.is_ascii());
653        assert!(!ZonedEncodingFormat::Ebcdic.is_ascii());
654        assert!(!ZonedEncodingFormat::Auto.is_ascii());
655    }
656
657    #[test]
658    fn test_zoned_encoding_format_is_ebcdic() {
659        assert!(!ZonedEncodingFormat::Ascii.is_ebcdic());
660        assert!(ZonedEncodingFormat::Ebcdic.is_ebcdic());
661        assert!(!ZonedEncodingFormat::Auto.is_ebcdic());
662    }
663
664    #[test]
665    fn test_zoned_encoding_format_is_auto() {
666        assert!(!ZonedEncodingFormat::Ascii.is_auto());
667        assert!(!ZonedEncodingFormat::Ebcdic.is_auto());
668        assert!(ZonedEncodingFormat::Auto.is_auto());
669    }
670
671    #[test]
672    fn test_zoned_encoding_format_description() {
673        assert_eq!(
674            ZonedEncodingFormat::Ascii.description(),
675            "ASCII digit zones (0x30-0x39)"
676        );
677        assert_eq!(
678            ZonedEncodingFormat::Ebcdic.description(),
679            "EBCDIC digit zones (0xF0-0xF9)"
680        );
681        assert_eq!(
682            ZonedEncodingFormat::Auto.description(),
683            "Automatic detection based on zone nibbles"
684        );
685    }
686
687    #[test]
688    fn test_zoned_encoding_format_detect_from_byte() {
689        // ASCII zone nibble (0x30)
690        assert_eq!(
691            ZonedEncodingFormat::detect_from_byte(0x35),
692            Some(ZonedEncodingFormat::Ascii)
693        );
694        assert_eq!(
695            ZonedEncodingFormat::detect_from_byte(0x30),
696            Some(ZonedEncodingFormat::Ascii)
697        );
698        assert_eq!(
699            ZonedEncodingFormat::detect_from_byte(0x39),
700            Some(ZonedEncodingFormat::Ascii)
701        );
702
703        // EBCDIC zone nibble (0xF0)
704        assert_eq!(
705            ZonedEncodingFormat::detect_from_byte(0xF5),
706            Some(ZonedEncodingFormat::Ebcdic)
707        );
708        assert_eq!(
709            ZonedEncodingFormat::detect_from_byte(0xF0),
710            Some(ZonedEncodingFormat::Ebcdic)
711        );
712        assert_eq!(
713            ZonedEncodingFormat::detect_from_byte(0xF9),
714            Some(ZonedEncodingFormat::Ebcdic)
715        );
716
717        // Invalid zone nibbles (0x00, 0x50)
718        assert_eq!(ZonedEncodingFormat::detect_from_byte(0x00), None);
719        assert_eq!(ZonedEncodingFormat::detect_from_byte(0x50), None);
720        // Note: 0xFF matches EBCDIC_ZONE (0x0F), so it returns Some(Ebcdic)
721        assert_eq!(
722            ZonedEncodingFormat::detect_from_byte(0xFF),
723            Some(ZonedEncodingFormat::Ebcdic)
724        );
725    }
726
727    #[test]
728    fn test_zoned_encoding_format_display() {
729        assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
730        assert_eq!(format!("{}", ZonedEncodingFormat::Ebcdic), "ebcdic");
731        assert_eq!(format!("{}", ZonedEncodingFormat::Auto), "auto");
732    }
733
734    #[test]
735    fn test_decode_options_default() {
736        let options = DecodeOptions::default();
737        assert_eq!(options.format, RecordFormat::Fixed);
738        assert_eq!(options.codepage, Codepage::CP037);
739        assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
740        assert!(!options.emit_filler);
741        assert!(!options.emit_meta);
742        assert_eq!(options.emit_raw, RawMode::Off);
743        assert!(!options.strict_mode);
744        assert!(options.max_errors.is_none());
745        assert_eq!(options.on_decode_unmappable, UnmappablePolicy::Error);
746        assert_eq!(options.threads, 1);
747        assert!(!options.preserve_zoned_encoding);
748        assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
749        assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
750    }
751
752    #[test]
753    fn test_encode_options_default() {
754        let options = EncodeOptions::default();
755        assert_eq!(options.format, RecordFormat::Fixed);
756        assert_eq!(options.codepage, Codepage::CP037);
757        assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
758        assert!(!options.use_raw);
759        assert!(!options.bwz_encode);
760        assert!(!options.strict_mode);
761        assert_eq!(options.on_encode_unmappable, UnmappablePolicy::Error);
762        assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
763        assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
764    }
765
766    #[test]
767    fn test_record_format_display() {
768        assert_eq!(format!("{}", RecordFormat::Fixed), "fixed");
769        assert_eq!(format!("{}", RecordFormat::RDW), "rdw");
770    }
771
772    #[test]
773    fn test_codepage_display() {
774        assert_eq!(format!("{}", Codepage::CP037), "cp037");
775        assert_eq!(format!("{}", Codepage::CP273), "cp273");
776        assert_eq!(format!("{}", Codepage::CP500), "cp500");
777        assert_eq!(format!("{}", Codepage::CP1047), "cp1047");
778        assert_eq!(format!("{}", Codepage::CP1140), "cp1140");
779    }
780
781    #[test]
782    fn test_json_number_mode_display() {
783        assert_eq!(format!("{}", JsonNumberMode::Lossless), "lossless");
784        assert_eq!(format!("{}", JsonNumberMode::Native), "native");
785    }
786
787    #[test]
788    fn test_raw_mode_display() {
789        assert_eq!(format!("{}", RawMode::Off), "off");
790        assert_eq!(format!("{}", RawMode::Record), "record");
791        assert_eq!(format!("{}", RawMode::Field), "field");
792        assert_eq!(format!("{}", RawMode::RecordRDW), "record+rdw");
793    }
794
795    #[test]
796    fn test_unmappable_policy_display() {
797        assert_eq!(format!("{}", UnmappablePolicy::Error), "error");
798        assert_eq!(format!("{}", UnmappablePolicy::Replace), "replace");
799        assert_eq!(format!("{}", UnmappablePolicy::Skip), "skip");
800    }
801
802    #[test]
803    fn test_decode_options_serialization() {
804        let options = DecodeOptions {
805            format: RecordFormat::Fixed,
806            codepage: Codepage::CP037,
807            json_number_mode: JsonNumberMode::Lossless,
808            emit_filler: true,
809            emit_meta: true,
810            emit_raw: RawMode::Record,
811            strict_mode: true,
812            max_errors: Some(100),
813            on_decode_unmappable: UnmappablePolicy::Replace,
814            threads: 4,
815            preserve_zoned_encoding: true,
816            preferred_zoned_encoding: ZonedEncodingFormat::Ebcdic,
817            float_format: FloatFormat::IbmHex,
818        };
819
820        let serialized = serde_json::to_string(&options).unwrap();
821        let deserialized: DecodeOptions = serde_json::from_str(&serialized).unwrap();
822
823        assert_eq!(deserialized.format, RecordFormat::Fixed);
824        assert_eq!(deserialized.codepage, Codepage::CP037);
825        assert!(deserialized.emit_filler);
826        assert!(deserialized.emit_meta);
827        assert_eq!(deserialized.emit_raw, RawMode::Record);
828        assert!(deserialized.strict_mode);
829        assert_eq!(deserialized.max_errors, Some(100));
830        assert_eq!(deserialized.on_decode_unmappable, UnmappablePolicy::Replace);
831        assert_eq!(deserialized.threads, 4);
832        assert!(deserialized.preserve_zoned_encoding);
833        assert_eq!(
834            deserialized.preferred_zoned_encoding,
835            ZonedEncodingFormat::Ebcdic
836        );
837        assert_eq!(deserialized.float_format, FloatFormat::IbmHex);
838    }
839
840    #[test]
841    fn test_decode_options_deserialize_missing_float_format_defaults() {
842        let options = DecodeOptions::default();
843        let mut value = serde_json::to_value(options).unwrap();
844        value.as_object_mut().unwrap().remove("float_format");
845        let deserialized: DecodeOptions = serde_json::from_value(value).unwrap();
846        assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
847    }
848
849    #[test]
850    fn test_encode_options_deserialize_missing_float_format_defaults() {
851        let options = EncodeOptions::default();
852        let mut value = serde_json::to_value(options).unwrap();
853        value.as_object_mut().unwrap().remove("float_format");
854        let deserialized: EncodeOptions = serde_json::from_value(value).unwrap();
855        assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
856    }
857}