use crate::{decode_record, encode_record, DecodeOptions, EncodeOptions, RawMode};
use copybook_core::{Schema, Result};
use serde_json::Value;
use sha2::{Digest, Sha256};
#[derive(Debug, Clone)]
pub struct RoundTripConfig {
pub schema: Schema,
pub original_data: Vec<u8>,
pub decode_options: DecodeOptions,
pub encode_options: EncodeOptions,
pub expected_hash: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RoundTripResult {
pub success: bool,
pub original_hash: String,
pub roundtrip_hash: String,
pub decoded_json: Value,
pub reencoded_data: Vec<u8>,
pub errors: Vec<String>,
}
impl RoundTripConfig {
pub fn new(schema: Schema, original_data: Vec<u8>) -> Self {
Self {
schema,
original_data,
decode_options: DecodeOptions::default(),
encode_options: EncodeOptions::default(),
expected_hash: None,
}
}
pub fn with_decode_options(mut self, options: DecodeOptions) -> Self {
self.decode_options = options;
self
}
pub fn with_encode_options(mut self, options: EncodeOptions) -> Self {
self.encode_options = options;
self
}
pub fn with_expected_hash(mut self, hash: String) -> Self {
self.expected_hash = Some(hash);
self
}
pub fn with_raw_capture(mut self) -> Self {
self.decode_options.emit_raw = RawMode::Record;
self.encode_options.use_raw = true;
self
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn run(&self) -> Result<RoundTripResult> {
let mut errors = Vec::new();
let decoded_json = match decode_record(&self.schema, &self.original_data, &self.decode_options) {
Ok(json) => json,
Err(e) => {
errors.push(format!("Decode failed: {}", e));
return Ok(RoundTripResult {
success: false,
original_hash: self.calculate_hash(&self.original_data),
roundtrip_hash: String::new(),
decoded_json: Value::Null,
reencoded_data: Vec::new(),
errors,
});
}
};
let reencoded_data = match encode_record(&self.schema, &decoded_json, &self.encode_options) {
Ok(data) => data,
Err(e) => {
errors.push(format!("Encode failed: {}", e));
return Ok(RoundTripResult {
success: false,
original_hash: self.calculate_hash(&self.original_data),
roundtrip_hash: String::new(),
decoded_json,
reencoded_data: Vec::new(),
errors,
});
}
};
let original_hash = self.calculate_hash(&self.original_data);
let roundtrip_hash = self.calculate_hash(&reencoded_data);
let success = if self.encode_options.use_raw && self.decode_options.emit_raw != RawMode::Off {
original_hash == roundtrip_hash
} else {
true
};
if let Some(ref expected) = self.expected_hash {
if &roundtrip_hash != expected {
errors.push(format!("Hash mismatch: expected {}, got {}", expected, roundtrip_hash));
}
}
Ok(RoundTripResult {
success: success && errors.is_empty(),
original_hash,
roundtrip_hash,
decoded_json,
reencoded_data,
errors,
})
}
fn calculate_hash(&self, data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
format!("{:x}", hasher.finalize())
}
}
pub struct RoundTripTestSuite {
tests: Vec<RoundTripConfig>,
}
impl RoundTripTestSuite {
pub fn new() -> Self {
Self {
tests: Vec::new(),
}
}
pub fn add_test(mut self, test: RoundTripConfig) -> Self {
self.tests.push(test);
self
}
pub fn run_all(&self) -> Vec<RoundTripResult> {
self.tests.iter().map(|test| test.run().unwrap_or_else(|e| {
RoundTripResult {
success: false,
original_hash: String::new(),
roundtrip_hash: String::new(),
decoded_json: Value::Null,
reencoded_data: Vec::new(),
errors: vec![format!("Test execution failed: {}", e)],
}
})).collect()
}
pub fn all_passed(&self) -> bool {
self.run_all().iter().all(|result| result.success)
}
}
impl Default for RoundTripTestSuite {
fn default() -> Self {
Self::new()
}
}
pub fn create_comprehensive_test_suite() -> RoundTripTestSuite {
let mut suite = RoundTripTestSuite::new();
if let Ok(schema) = create_simple_alphanum_schema() {
let test_data = b"HELLO WORLD "; let config = RoundTripConfig::new(schema, test_data.to_vec())
.with_raw_capture();
suite = suite.add_test(config);
}
if let Ok(schema) = create_zoned_decimal_schema() {
let test_data = b"001234C"; let config = RoundTripConfig::new(schema, test_data.to_vec())
.with_raw_capture();
suite = suite.add_test(config);
}
if let Ok(schema) = create_packed_decimal_schema() {
let test_data = b"\x01\x23\x4C"; let config = RoundTripConfig::new(schema, test_data.to_vec())
.with_raw_capture();
suite = suite.add_test(config);
}
if let Ok(schema) = create_binary_int_schema() {
let test_data = b"\x00\x00\x04\xD2"; let config = RoundTripConfig::new(schema, test_data.to_vec())
.with_raw_capture();
suite = suite.add_test(config);
}
if let Ok(schema) = create_redefines_schema() {
let test_data = b"ABCD1234"; let config = RoundTripConfig::new(schema, test_data.to_vec())
.with_raw_capture();
suite = suite.add_test(config);
}
suite
}
fn create_simple_alphanum_schema() -> Result<Schema> {
use copybook_core::{Field, FieldKind};
let field = Field {
path: "ROOT.NAME".to_string(),
name: "NAME".to_string(),
level: 1,
kind: FieldKind::Alphanum { len: 16 },
offset: 0,
len: 16,
redefines_of: None,
occurs: None,
sync_padding: None,
synchronized: false,
blank_when_zero: false,
children: Vec::new(),
};
Ok(Schema::from_fields(vec![field]))
}
fn create_zoned_decimal_schema() -> Result<Schema> {
use copybook_core::{Field, FieldKind};
let field = Field {
path: "ROOT.AMOUNT".to_string(),
name: "AMOUNT".to_string(),
level: 1,
kind: FieldKind::ZonedDecimal { digits: 6, scale: 2, signed: true },
offset: 0,
len: 7, redefines_of: None,
occurs: None,
sync_padding: None,
synchronized: false,
blank_when_zero: false,
children: Vec::new(),
};
Ok(Schema::from_fields(vec![field]))
}
fn create_packed_decimal_schema() -> Result<Schema> {
use copybook_core::{Field, FieldKind};
let field = Field {
path: "ROOT.PACKED_AMT".to_string(),
name: "PACKED_AMT".to_string(),
level: 1,
kind: FieldKind::PackedDecimal { digits: 4, scale: 0, signed: true },
offset: 0,
len: 3, redefines_of: None,
occurs: None,
sync_padding: None,
synchronized: false,
blank_when_zero: false,
children: Vec::new(),
};
Ok(Schema::from_fields(vec![field]))
}
fn create_binary_int_schema() -> Result<Schema> {
use copybook_core::{Field, FieldKind};
let field = Field {
path: "ROOT.BINARY_NUM".to_string(),
name: "BINARY_NUM".to_string(),
level: 1,
kind: FieldKind::BinaryInt { bits: 32, signed: false },
offset: 0,
len: 4,
redefines_of: None,
occurs: None,
sync_padding: None,
synchronized: false,
blank_when_zero: false,
children: Vec::new(),
};
Ok(Schema::from_fields(vec![field]))
}
fn create_redefines_schema() -> Result<Schema> {
use copybook_core::{Field, FieldKind};
let primary_field = Field {
path: "ROOT.DATA_AREA".to_string(),
name: "DATA_AREA".to_string(),
level: 1,
kind: FieldKind::Alphanum { len: 8 },
offset: 0,
len: 8,
redefines_of: None,
occurs: None,
sync_padding: None,
synchronized: false,
blank_when_zero: false,
children: Vec::new(),
};
let redefining_field = Field {
path: "ROOT.NUMERIC_VIEW".to_string(),
name: "NUMERIC_VIEW".to_string(),
level: 1,
kind: FieldKind::ZonedDecimal { digits: 8, scale: 0, signed: false },
offset: 0,
len: 8,
redefines_of: Some("ROOT.DATA_AREA".to_string()),
occurs: None,
sync_padding: None,
synchronized: false,
blank_when_zero: false,
children: Vec::new(),
};
Ok(Schema::from_fields(vec![primary_field, redefining_field]))
}
#[cfg(test)]
#[allow(clippy::expect_used)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn test_simple_round_trip() {
let suite = create_comprehensive_test_suite();
let results = suite.run_all();
}
#[test]
fn test_raw_data_preservation() {
if let Ok(schema) = create_simple_alphanum_schema() {
let original_data = b"TEST DATA ";
let decode_opts = DecodeOptions {
emit_raw: RawMode::Record,
..DecodeOptions::default()
};
let encode_opts = EncodeOptions {
use_raw: true,
..EncodeOptions::default()
};
let config = RoundTripConfig::new(schema, original_data.to_vec())
.with_decode_options(decode_opts)
.with_encode_options(encode_opts);
let result = config.run().expect("Round-trip test should not fail");
if result.success {
assert_eq!(result.original_hash, result.roundtrip_hash,
"Raw data round-trip should be byte-identical");
}
}
}
#[test]
fn test_redefines_encoding_precedence() {
if let Ok(schema) = create_simple_alphanum_schema() {
let original_data = b"TEST DATA ";
let decode_opts = DecodeOptions {
emit_raw: RawMode::Record,
codepage: crate::options::Codepage::ASCII,
..DecodeOptions::default()
};
let json = decode_record(&schema, original_data, &decode_opts)
.expect("Decode should succeed");
let encode_opts_with_raw = EncodeOptions {
use_raw: true,
codepage: crate::options::Codepage::ASCII,
..EncodeOptions::default()
};
let result_with_raw = encode_record(&schema, &json, &encode_opts_with_raw);
assert!(result_with_raw.is_ok(), "Encoding with raw should succeed");
if let Ok(reencoded) = result_with_raw {
assert_eq!(original_data, reencoded.as_slice(),
"Raw data round-trip should be byte-identical");
}
}
}
}