#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
#![allow(clippy::missing_inline_in_public_items)]
pub use copybook_charset::Codepage;
pub use copybook_charset::UnmappablePolicy;
pub use copybook_zoned_format::ZonedEncodingFormat;
use serde::{Deserialize, Serialize};
use std::fmt;
const DEFAULT_RECORD_FORMAT: RecordFormat = RecordFormat::Fixed;
const DEFAULT_CODEPAGE: Codepage = Codepage::CP037;
const DEFAULT_JSON_NUMBER_MODE: JsonNumberMode = JsonNumberMode::Lossless;
const DEFAULT_UNMAPPABLE_POLICY: UnmappablePolicy = UnmappablePolicy::Error;
const DEFAULT_THREAD_COUNT: usize = 1;
const DEFAULT_ZONED_ENCODING: ZonedEncodingFormat = ZonedEncodingFormat::Auto;
const DEFAULT_FLOAT_FORMAT: FloatFormat = FloatFormat::IeeeBigEndian;
macro_rules! impl_common_option_builders {
() => {
#[must_use]
#[inline]
pub fn with_format(mut self, format: RecordFormat) -> Self {
self.format = format;
self
}
#[must_use]
#[inline]
pub fn with_codepage(mut self, codepage: Codepage) -> Self {
self.codepage = codepage;
self
}
#[must_use]
#[inline]
pub fn with_strict_mode(mut self, strict_mode: bool) -> Self {
self.strict_mode = strict_mode;
self
}
#[must_use]
#[inline]
pub fn with_max_errors(mut self, max_errors: Option<u64>) -> Self {
self.max_errors = max_errors;
self
}
#[must_use]
#[inline]
pub fn with_threads(mut self, threads: usize) -> Self {
self.threads = threads;
self
}
#[must_use]
#[inline]
pub fn with_json_number_mode(mut self, mode: JsonNumberMode) -> Self {
self.json_number_mode = mode;
self
}
#[must_use]
#[inline]
pub fn with_preferred_zoned_encoding(
mut self,
preferred_zoned_encoding: ZonedEncodingFormat,
) -> Self {
self.preferred_zoned_encoding = preferred_zoned_encoding;
self
}
#[must_use]
#[inline]
pub fn with_float_format(mut self, float_format: FloatFormat) -> Self {
self.float_format = float_format;
self
}
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum, Default)]
pub enum FloatFormat {
#[default]
#[value(name = "ieee-be", alias = "ieee", alias = "ieee-big-endian")]
IeeeBigEndian,
#[value(name = "ibm-hex", alias = "ibm")]
IbmHex,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)] pub struct DecodeOptions {
pub format: RecordFormat,
pub codepage: Codepage,
pub json_number_mode: JsonNumberMode,
pub emit_filler: bool,
pub emit_meta: bool,
pub emit_raw: RawMode,
pub strict_mode: bool,
pub max_errors: Option<u64>,
pub on_decode_unmappable: UnmappablePolicy,
pub threads: usize,
pub preserve_zoned_encoding: bool,
pub preferred_zoned_encoding: ZonedEncodingFormat,
#[serde(default)]
pub float_format: FloatFormat,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct EncodeOptions {
pub format: RecordFormat,
pub codepage: Codepage,
pub preferred_zoned_encoding: ZonedEncodingFormat,
pub use_raw: bool,
pub bwz_encode: bool,
pub strict_mode: bool,
pub max_errors: Option<u64>,
pub threads: usize,
pub coerce_numbers: bool,
pub on_encode_unmappable: UnmappablePolicy,
pub json_number_mode: JsonNumberMode,
pub zoned_encoding_override: Option<ZonedEncodingFormat>,
#[serde(default)]
pub float_format: FloatFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
pub enum RecordFormat {
Fixed,
RDW,
}
impl RecordFormat {
#[must_use]
pub const fn is_fixed(self) -> bool {
matches!(self, Self::Fixed)
}
#[must_use]
pub const fn is_variable(self) -> bool {
matches!(self, Self::RDW)
}
#[must_use]
pub const fn description(self) -> &'static str {
match self {
Self::Fixed => "Fixed-length records",
Self::RDW => "Variable-length records with Record Descriptor Word",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
pub enum JsonNumberMode {
Lossless,
Native,
}
impl JsonNumberMode {
#[must_use]
#[inline]
pub const fn is_lossless(self) -> bool {
matches!(self, Self::Lossless)
}
#[must_use]
#[inline]
pub const fn is_native(self) -> bool {
matches!(self, Self::Native)
}
#[must_use]
#[inline]
pub const fn description(self) -> &'static str {
match self {
Self::Lossless => "Lossless string representation for decimals",
Self::Native => "Native JSON numbers where possible",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
pub enum RawMode {
Off,
Record,
Field,
#[value(name = "record+rdw")]
RecordRDW,
}
impl Default for DecodeOptions {
fn default() -> Self {
Self {
format: DEFAULT_RECORD_FORMAT,
codepage: DEFAULT_CODEPAGE,
json_number_mode: DEFAULT_JSON_NUMBER_MODE,
emit_filler: false,
emit_meta: false,
emit_raw: RawMode::Off,
strict_mode: false,
max_errors: None,
on_decode_unmappable: DEFAULT_UNMAPPABLE_POLICY,
threads: DEFAULT_THREAD_COUNT,
preserve_zoned_encoding: false,
preferred_zoned_encoding: DEFAULT_ZONED_ENCODING,
float_format: DEFAULT_FLOAT_FORMAT,
}
}
}
impl DecodeOptions {
#[must_use]
#[inline]
pub fn new() -> Self {
Self::default()
}
impl_common_option_builders!();
#[must_use]
#[inline]
pub fn with_emit_filler(mut self, emit_filler: bool) -> Self {
self.emit_filler = emit_filler;
self
}
#[must_use]
#[inline]
pub fn with_emit_meta(mut self, emit_meta: bool) -> Self {
self.emit_meta = emit_meta;
self
}
#[must_use]
#[inline]
pub fn with_emit_raw(mut self, emit_raw: RawMode) -> Self {
self.emit_raw = emit_raw;
self
}
#[must_use]
#[inline]
pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
self.on_decode_unmappable = policy;
self
}
#[must_use]
#[inline]
pub fn with_preserve_zoned_encoding(mut self, preserve_zoned_encoding: bool) -> Self {
self.preserve_zoned_encoding = preserve_zoned_encoding;
self
}
}
impl Default for EncodeOptions {
fn default() -> Self {
Self {
format: DEFAULT_RECORD_FORMAT,
codepage: DEFAULT_CODEPAGE,
preferred_zoned_encoding: DEFAULT_ZONED_ENCODING,
use_raw: false,
bwz_encode: false,
strict_mode: false,
max_errors: None,
threads: DEFAULT_THREAD_COUNT,
coerce_numbers: false,
on_encode_unmappable: DEFAULT_UNMAPPABLE_POLICY,
json_number_mode: DEFAULT_JSON_NUMBER_MODE,
zoned_encoding_override: None,
float_format: DEFAULT_FLOAT_FORMAT,
}
}
}
impl EncodeOptions {
#[must_use]
#[inline]
pub fn new() -> Self {
Self::default()
}
impl_common_option_builders!();
#[must_use]
#[inline]
pub fn with_use_raw(mut self, use_raw: bool) -> Self {
self.use_raw = use_raw;
self
}
#[must_use]
#[inline]
pub fn with_bwz_encode(mut self, bwz_encode: bool) -> Self {
self.bwz_encode = bwz_encode;
self
}
#[must_use]
#[inline]
pub fn with_coerce_numbers(mut self, coerce_numbers: bool) -> Self {
self.coerce_numbers = coerce_numbers;
self
}
#[must_use]
#[inline]
pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
self.on_encode_unmappable = policy;
self
}
#[must_use]
#[inline]
pub fn with_zoned_encoding_override(
mut self,
zoned_encoding_override: Option<ZonedEncodingFormat>,
) -> Self {
self.zoned_encoding_override = zoned_encoding_override;
self
}
#[must_use]
#[inline]
pub fn with_zoned_encoding_format(mut self, format: ZonedEncodingFormat) -> Self {
self.zoned_encoding_override = Some(format);
self
}
}
impl fmt::Display for RecordFormat {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Fixed => write!(f, "fixed"),
Self::RDW => write!(f, "rdw"),
}
}
}
impl fmt::Display for JsonNumberMode {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Lossless => write!(f, "lossless"),
Self::Native => write!(f, "native"),
}
}
}
impl fmt::Display for RawMode {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Off => write!(f, "off"),
Self::Record => write!(f, "record"),
Self::Field => write!(f, "field"),
Self::RecordRDW => write!(f, "record+rdw"),
}
}
}
impl fmt::Display for FloatFormat {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IeeeBigEndian => write!(f, "ieee-be"),
Self::IbmHex => write!(f, "ibm-hex"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zoned_encoding_format_is_ascii() {
assert!(ZonedEncodingFormat::Ascii.is_ascii());
assert!(!ZonedEncodingFormat::Ebcdic.is_ascii());
assert!(!ZonedEncodingFormat::Auto.is_ascii());
}
#[test]
fn test_zoned_encoding_format_is_ebcdic() {
assert!(!ZonedEncodingFormat::Ascii.is_ebcdic());
assert!(ZonedEncodingFormat::Ebcdic.is_ebcdic());
assert!(!ZonedEncodingFormat::Auto.is_ebcdic());
}
#[test]
fn test_zoned_encoding_format_is_auto() {
assert!(!ZonedEncodingFormat::Ascii.is_auto());
assert!(!ZonedEncodingFormat::Ebcdic.is_auto());
assert!(ZonedEncodingFormat::Auto.is_auto());
}
#[test]
fn test_zoned_encoding_format_description() {
assert_eq!(
ZonedEncodingFormat::Ascii.description(),
"ASCII digit zones (0x30-0x39)"
);
assert_eq!(
ZonedEncodingFormat::Ebcdic.description(),
"EBCDIC digit zones (0xF0-0xF9)"
);
assert_eq!(
ZonedEncodingFormat::Auto.description(),
"Automatic detection based on zone nibbles"
);
}
#[test]
fn test_zoned_encoding_format_detect_from_byte() {
assert_eq!(
ZonedEncodingFormat::detect_from_byte(0x35),
Some(ZonedEncodingFormat::Ascii)
);
assert_eq!(
ZonedEncodingFormat::detect_from_byte(0x30),
Some(ZonedEncodingFormat::Ascii)
);
assert_eq!(
ZonedEncodingFormat::detect_from_byte(0x39),
Some(ZonedEncodingFormat::Ascii)
);
assert_eq!(
ZonedEncodingFormat::detect_from_byte(0xF5),
Some(ZonedEncodingFormat::Ebcdic)
);
assert_eq!(
ZonedEncodingFormat::detect_from_byte(0xF0),
Some(ZonedEncodingFormat::Ebcdic)
);
assert_eq!(
ZonedEncodingFormat::detect_from_byte(0xF9),
Some(ZonedEncodingFormat::Ebcdic)
);
assert_eq!(ZonedEncodingFormat::detect_from_byte(0x00), None);
assert_eq!(ZonedEncodingFormat::detect_from_byte(0x50), None);
assert_eq!(
ZonedEncodingFormat::detect_from_byte(0xFF),
Some(ZonedEncodingFormat::Ebcdic)
);
}
#[test]
fn test_zoned_encoding_format_display() {
assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
assert_eq!(format!("{}", ZonedEncodingFormat::Ebcdic), "ebcdic");
assert_eq!(format!("{}", ZonedEncodingFormat::Auto), "auto");
}
#[test]
fn test_decode_options_default() {
let options = DecodeOptions::default();
assert_eq!(options.format, RecordFormat::Fixed);
assert_eq!(options.codepage, Codepage::CP037);
assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
assert!(!options.emit_filler);
assert!(!options.emit_meta);
assert_eq!(options.emit_raw, RawMode::Off);
assert!(!options.strict_mode);
assert!(options.max_errors.is_none());
assert_eq!(options.on_decode_unmappable, UnmappablePolicy::Error);
assert_eq!(options.threads, 1);
assert!(!options.preserve_zoned_encoding);
assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
}
#[test]
fn test_encode_options_default() {
let options = EncodeOptions::default();
assert_eq!(options.format, RecordFormat::Fixed);
assert_eq!(options.codepage, Codepage::CP037);
assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
assert!(!options.use_raw);
assert!(!options.bwz_encode);
assert!(!options.strict_mode);
assert_eq!(options.on_encode_unmappable, UnmappablePolicy::Error);
assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
}
#[test]
fn test_record_format_display() {
assert_eq!(format!("{}", RecordFormat::Fixed), "fixed");
assert_eq!(format!("{}", RecordFormat::RDW), "rdw");
}
#[test]
fn test_codepage_display() {
assert_eq!(format!("{}", Codepage::CP037), "cp037");
assert_eq!(format!("{}", Codepage::CP273), "cp273");
assert_eq!(format!("{}", Codepage::CP500), "cp500");
assert_eq!(format!("{}", Codepage::CP1047), "cp1047");
assert_eq!(format!("{}", Codepage::CP1140), "cp1140");
}
#[test]
fn test_json_number_mode_display() {
assert_eq!(format!("{}", JsonNumberMode::Lossless), "lossless");
assert_eq!(format!("{}", JsonNumberMode::Native), "native");
}
#[test]
fn test_raw_mode_display() {
assert_eq!(format!("{}", RawMode::Off), "off");
assert_eq!(format!("{}", RawMode::Record), "record");
assert_eq!(format!("{}", RawMode::Field), "field");
assert_eq!(format!("{}", RawMode::RecordRDW), "record+rdw");
}
#[test]
fn test_unmappable_policy_display() {
assert_eq!(format!("{}", UnmappablePolicy::Error), "error");
assert_eq!(format!("{}", UnmappablePolicy::Replace), "replace");
assert_eq!(format!("{}", UnmappablePolicy::Skip), "skip");
}
#[test]
fn test_decode_options_serialization() {
let options = DecodeOptions {
format: DEFAULT_RECORD_FORMAT,
codepage: DEFAULT_CODEPAGE,
json_number_mode: DEFAULT_JSON_NUMBER_MODE,
emit_filler: true,
emit_meta: true,
emit_raw: RawMode::Record,
strict_mode: true,
max_errors: Some(100),
on_decode_unmappable: UnmappablePolicy::Replace,
threads: 4,
preserve_zoned_encoding: true,
preferred_zoned_encoding: ZonedEncodingFormat::Ebcdic,
float_format: FloatFormat::IbmHex,
};
let serialized = serde_json::to_string(&options).unwrap();
let deserialized: DecodeOptions = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.format, RecordFormat::Fixed);
assert_eq!(deserialized.codepage, Codepage::CP037);
assert!(deserialized.emit_filler);
assert!(deserialized.emit_meta);
assert_eq!(deserialized.emit_raw, RawMode::Record);
assert!(deserialized.strict_mode);
assert_eq!(deserialized.max_errors, Some(100));
assert_eq!(deserialized.on_decode_unmappable, UnmappablePolicy::Replace);
assert_eq!(deserialized.threads, 4);
assert!(deserialized.preserve_zoned_encoding);
assert_eq!(
deserialized.preferred_zoned_encoding,
ZonedEncodingFormat::Ebcdic
);
assert_eq!(deserialized.float_format, FloatFormat::IbmHex);
}
#[test]
fn test_decode_options_deserialize_missing_float_format_defaults() {
let options = DecodeOptions::default();
let mut value = serde_json::to_value(options).unwrap();
value.as_object_mut().unwrap().remove("float_format");
let deserialized: DecodeOptions = serde_json::from_value(value).unwrap();
assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
}
#[test]
fn test_encode_options_deserialize_missing_float_format_defaults() {
let options = EncodeOptions::default();
let mut value = serde_json::to_value(options).unwrap();
value.as_object_mut().unwrap().remove("float_format");
let deserialized: EncodeOptions = serde_json::from_value(value).unwrap();
assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
}
}