use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Schema {
pub fields: Vec<Field>,
pub lrecl_fixed: Option<u32>,
pub tail_odo: Option<TailODO>,
pub fingerprint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TailODO {
pub counter_path: String,
pub min_count: u32,
pub max_count: u32,
pub array_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Field {
pub path: String,
pub name: String,
pub level: u8,
pub kind: FieldKind,
pub offset: u32,
pub len: u32,
pub redefines_of: Option<String>,
pub occurs: Option<Occurs>,
pub sync_padding: Option<u16>,
pub synchronized: bool,
pub blank_when_zero: bool,
pub resolved_renames: Option<ResolvedRenames>,
pub children: Vec<Field>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FieldKind {
Alphanum {
len: u32,
},
ZonedDecimal {
digits: u16,
scale: i16,
signed: bool,
sign_separate: Option<SignSeparateInfo>,
},
BinaryInt {
bits: u16,
signed: bool,
},
PackedDecimal {
digits: u16,
scale: i16,
signed: bool,
},
Group,
Condition {
values: Vec<String>,
},
Renames {
from_field: String,
thru_field: String,
},
EditedNumeric {
pic_string: String,
width: u16,
scale: u16,
signed: bool,
},
FloatSingle,
FloatDouble,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedRenames {
pub offset: u32,
pub length: u32,
pub members: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignSeparateInfo {
pub placement: SignPlacement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SignPlacement {
Leading,
Trailing,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Occurs {
Fixed {
count: u32,
},
ODO {
min: u32,
max: u32,
counter_path: String,
},
}
impl Schema {
#[must_use]
pub fn new() -> Self {
Self {
fields: Vec::new(),
lrecl_fixed: None,
tail_odo: None,
fingerprint: String::new(),
}
}
#[must_use]
pub fn from_fields(fields: Vec<Field>) -> Self {
let mut schema = Self {
fields,
lrecl_fixed: None,
tail_odo: None,
fingerprint: String::new(),
};
schema.calculate_fingerprint();
schema
}
pub fn calculate_fingerprint(&mut self) {
use sha2::{Digest, Sha256};
let canonical_json = self.create_canonical_json();
let mut hasher = Sha256::new();
hasher.update(canonical_json.as_bytes());
let result = hasher.finalize();
self.fingerprint = format!("{result:x}");
}
pub fn create_canonical_json(&self) -> String {
use serde_json::{Map, Value};
let mut schema_obj = Map::new();
let fields_json: Vec<Value> = self
.fields
.iter()
.map(Self::field_to_canonical_json)
.collect();
schema_obj.insert("fields".to_string(), Value::Array(fields_json));
if let Some(lrecl) = self.lrecl_fixed {
schema_obj.insert("lrecl_fixed".to_string(), Value::Number(lrecl.into()));
}
if let Some(ref tail_odo) = self.tail_odo {
let mut tail_odo_obj = Map::new();
tail_odo_obj.insert(
"counter_path".to_string(),
Value::String(tail_odo.counter_path.clone()),
);
tail_odo_obj.insert(
"min_count".to_string(),
Value::Number(tail_odo.min_count.into()),
);
tail_odo_obj.insert(
"max_count".to_string(),
Value::Number(tail_odo.max_count.into()),
);
tail_odo_obj.insert(
"array_path".to_string(),
Value::String(tail_odo.array_path.clone()),
);
schema_obj.insert("tail_odo".to_string(), Value::Object(tail_odo_obj));
}
serde_json::to_string(&Value::Object(schema_obj)).unwrap_or_default()
}
fn field_to_canonical_json(field: &Field) -> Value {
use serde_json::{Map, Value};
let mut field_obj = Map::new();
field_obj.insert("path".to_string(), Value::String(field.path.clone()));
field_obj.insert("name".to_string(), Value::String(field.name.clone()));
field_obj.insert("level".to_string(), Value::Number(field.level.into()));
let kind_str = match &field.kind {
FieldKind::Alphanum { len } => format!("Alphanum({len})"),
FieldKind::ZonedDecimal {
digits,
scale,
signed,
sign_separate,
} => {
format!("ZonedDecimal({digits},{scale},{signed},{sign_separate:?})")
}
FieldKind::BinaryInt { bits, signed } => {
format!("BinaryInt({bits},{signed})")
}
FieldKind::PackedDecimal {
digits,
scale,
signed,
} => {
format!("PackedDecimal({digits},{scale},{signed})")
}
FieldKind::Group => "Group".to_string(),
FieldKind::Condition { values } => format!("Condition({values:?})"),
FieldKind::Renames {
from_field,
thru_field,
} => format!("Renames({from_field},{thru_field})"),
FieldKind::EditedNumeric {
pic_string,
width,
scale,
signed,
} => {
format!("EditedNumeric({pic_string},{width},scale={scale},signed={signed})")
}
FieldKind::FloatSingle => "FloatSingle".to_string(),
FieldKind::FloatDouble => "FloatDouble".to_string(),
};
field_obj.insert("kind".to_string(), Value::String(kind_str));
if let Some(ref redefines) = field.redefines_of {
field_obj.insert("redefines_of".to_string(), Value::String(redefines.clone()));
}
if let Some(ref occurs) = field.occurs {
let occurs_str = match occurs {
Occurs::Fixed { count } => format!("Fixed({count})"),
Occurs::ODO {
min,
max,
counter_path,
} => {
format!("ODO({min},{max},{counter_path})")
}
};
field_obj.insert("occurs".to_string(), Value::String(occurs_str));
}
if field.synchronized {
field_obj.insert("synchronized".to_string(), Value::Bool(true));
}
if field.blank_when_zero {
field_obj.insert("blank_when_zero".to_string(), Value::Bool(true));
}
if !field.children.is_empty() {
let children_json: Vec<Value> = field
.children
.iter()
.map(Self::field_to_canonical_json)
.collect();
field_obj.insert("children".to_string(), Value::Array(children_json));
}
Value::Object(field_obj)
}
#[must_use]
pub fn find_field(&self, path: &str) -> Option<&Field> {
Self::find_field_recursive(&self.fields, path)
}
fn find_field_recursive<'a>(fields: &'a [Field], path: &str) -> Option<&'a Field> {
for field in fields {
if field.path == path {
return Some(field);
}
if let Some(found) = Self::find_field_recursive(&field.children, path) {
return Some(found);
}
}
None
}
#[must_use]
pub fn find_field_or_alias(&self, name_or_path: &str) -> Option<&Field> {
if let Some(field) = self.find_field(name_or_path) {
return Some(field);
}
let query_name = name_or_path.rsplit('.').next().unwrap_or(name_or_path);
Self::find_alias_field_recursive(&self.fields, query_name)
}
fn find_alias_field_recursive<'a>(fields: &'a [Field], alias_name: &str) -> Option<&'a Field> {
for field in fields {
if field.level == 66 && field.name.eq_ignore_ascii_case(alias_name) {
return Some(field);
}
if let Some(found) = Self::find_alias_field_recursive(&field.children, alias_name) {
return Some(found);
}
}
None
}
#[must_use]
pub fn resolve_alias_to_target(&self, name_or_path: &str) -> Option<&Field> {
if let Some(alias_field) = self.find_field_or_alias(name_or_path) {
if alias_field.level == 66
&& let Some(ref resolved) = alias_field.resolved_renames
&& let Some(first_member_path) = resolved.members.first()
{
return self.find_field(first_member_path);
}
return Some(alias_field);
}
None
}
#[must_use]
pub fn find_redefining_fields<'a>(&'a self, target_path: &str) -> Vec<&'a Field> {
fn collect<'a>(fields: &'a [Field], target_path: &str, acc: &mut Vec<&'a Field>) {
for f in fields {
if let Some(ref redef) = f.redefines_of
&& redef == target_path
{
acc.push(f);
}
collect(&f.children, target_path, acc);
}
}
let mut result = Vec::new();
collect(&self.fields, target_path, &mut result);
result
}
#[must_use]
pub fn all_fields(&self) -> Vec<&Field> {
let mut result = Vec::new();
Self::collect_fields_recursive(&self.fields, &mut result);
result
}
fn collect_fields_recursive<'a>(fields: &'a [Field], result: &mut Vec<&'a Field>) {
for field in fields {
result.push(field);
Self::collect_fields_recursive(&field.children, result);
}
}
}
impl Field {
#[must_use]
pub fn new(level: u8, name: String) -> Self {
Self {
path: name.clone(),
name,
level,
kind: FieldKind::Group, offset: 0,
len: 0,
redefines_of: None,
occurs: None,
sync_padding: None,
synchronized: false,
blank_when_zero: false,
resolved_renames: None,
children: Vec::new(),
}
}
#[must_use]
pub fn with_kind(level: u8, name: String, kind: FieldKind) -> Self {
Self {
path: name.clone(),
name,
level,
kind,
offset: 0,
len: 0,
redefines_of: None,
occurs: None,
sync_padding: None,
synchronized: false,
blank_when_zero: false,
resolved_renames: None,
children: Vec::new(),
}
}
#[must_use]
pub fn is_group(&self) -> bool {
matches!(self.kind, FieldKind::Group)
}
#[must_use]
pub fn is_scalar(&self) -> bool {
!self.is_group()
}
#[must_use]
pub fn effective_length(&self) -> u32 {
match &self.occurs {
Some(Occurs::Fixed { count }) => self.len * count,
Some(Occurs::ODO { max, .. }) => self.len * max,
None => self.len,
}
}
#[must_use]
pub fn sign_separate(&self) -> Option<&SignSeparateInfo> {
if let FieldKind::ZonedDecimal { sign_separate, .. } = &self.kind {
sign_separate.as_ref()
} else {
None
}
}
#[must_use]
pub fn is_packed(&self) -> bool {
matches!(self.kind, FieldKind::PackedDecimal { .. })
}
#[must_use]
pub fn is_binary(&self) -> bool {
matches!(self.kind, FieldKind::BinaryInt { .. })
}
#[must_use]
pub fn is_filler(&self) -> bool {
self.name.eq_ignore_ascii_case("FILLER")
}
}
impl Default for Schema {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schema_default() {
let schema = Schema::default();
assert!(schema.fields.is_empty());
assert!(schema.lrecl_fixed.is_none());
assert!(schema.tail_odo.is_none());
assert!(schema.fingerprint.is_empty());
}
#[test]
fn test_schema_new() {
let schema = Schema::new();
assert!(schema.fields.is_empty());
assert!(schema.lrecl_fixed.is_none());
assert!(schema.tail_odo.is_none());
}
#[test]
fn test_tail_odo_serialization() {
let tail_odo = TailODO {
counter_path: "ROOT.COUNT".to_string(),
min_count: 1,
max_count: 100,
array_path: "ROOT.ARRAY".to_string(),
};
let serialized = serde_json::to_string(&tail_odo).unwrap();
let deserialized: TailODO = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.counter_path, "ROOT.COUNT");
assert_eq!(deserialized.min_count, 1);
assert_eq!(deserialized.max_count, 100);
assert_eq!(deserialized.array_path, "ROOT.ARRAY");
}
#[test]
fn test_field_new() {
let field = Field::new(5, "TEST-FIELD".to_string());
assert_eq!(field.level, 5);
assert_eq!(field.name, "TEST-FIELD");
assert_eq!(field.path, "TEST-FIELD");
assert!(matches!(field.kind, FieldKind::Group));
assert_eq!(field.offset, 0);
assert_eq!(field.len, 0);
}
#[test]
fn test_field_with_kind() {
let kind = FieldKind::Alphanum { len: 10 };
let field = Field::with_kind(5, "TEST-FIELD".to_string(), kind.clone());
assert_eq!(field.level, 5);
assert_eq!(field.name, "TEST-FIELD");
assert!(matches!(field.kind, FieldKind::Alphanum { len: 10 }));
}
#[test]
fn test_field_is_group() {
let group_field = Field::new(1, "GROUP".to_string());
assert!(group_field.is_group());
assert!(!group_field.is_scalar());
let scalar_field =
Field::with_kind(5, "SCALAR".to_string(), FieldKind::Alphanum { len: 10 });
assert!(!scalar_field.is_group());
assert!(scalar_field.is_scalar());
}
#[test]
fn test_field_effective_length_no_occurs() {
let field = Field {
path: "TEST".to_string(),
name: "TEST".to_string(),
level: 5,
kind: FieldKind::Alphanum { len: 10 },
offset: 0,
len: 10,
redefines_of: None,
occurs: None,
sync_padding: None,
synchronized: false,
blank_when_zero: false,
resolved_renames: None,
children: Vec::new(),
};
assert_eq!(field.effective_length(), 10);
}
#[test]
fn test_field_effective_length_fixed_occurs() {
let field = Field {
path: "TEST".to_string(),
name: "TEST".to_string(),
level: 5,
kind: FieldKind::Alphanum { len: 10 },
offset: 0,
len: 10,
redefines_of: None,
occurs: Some(Occurs::Fixed { count: 5 }),
sync_padding: None,
synchronized: false,
blank_when_zero: false,
resolved_renames: None,
children: Vec::new(),
};
assert_eq!(field.effective_length(), 50);
}
#[test]
fn test_field_effective_length_odo_occurs() {
let field = Field {
path: "TEST".to_string(),
name: "TEST".to_string(),
level: 5,
kind: FieldKind::Alphanum { len: 10 },
offset: 0,
len: 10,
redefines_of: None,
occurs: Some(Occurs::ODO {
min: 1,
max: 100,
counter_path: "ROOT.COUNT".to_string(),
}),
sync_padding: None,
synchronized: false,
blank_when_zero: false,
resolved_renames: None,
children: Vec::new(),
};
assert_eq!(field.effective_length(), 1000);
}
#[test]
fn test_field_kind_serialization() {
let kinds = vec![
FieldKind::Alphanum { len: 10 },
FieldKind::ZonedDecimal {
digits: 5,
scale: 2,
signed: true,
sign_separate: None,
},
FieldKind::BinaryInt {
bits: 32,
signed: true,
},
FieldKind::PackedDecimal {
digits: 7,
scale: 2,
signed: true,
},
FieldKind::Group,
FieldKind::Condition {
values: vec!["A".to_string(), "B".to_string()],
},
FieldKind::Renames {
from_field: "FIELD1".to_string(),
thru_field: "FIELD2".to_string(),
},
FieldKind::EditedNumeric {
pic_string: "ZZ9.99".to_string(),
width: 6,
scale: 2,
signed: false,
},
];
for kind in kinds {
let serialized = serde_json::to_string(&kind).unwrap();
let deserialized: FieldKind = serde_json::from_str(&serialized).unwrap();
let re_serialized = serde_json::to_string(&deserialized).unwrap();
assert_eq!(serialized, re_serialized);
}
}
#[test]
fn test_sign_placement_serialization() {
let placements = vec![SignPlacement::Leading, SignPlacement::Trailing];
for placement in placements {
let serialized = serde_json::to_string(&placement).unwrap();
let deserialized: SignPlacement = serde_json::from_str(&serialized).unwrap();
assert_eq!(serialized, serde_json::to_string(&deserialized).unwrap());
}
}
#[test]
fn test_sign_separate_info_serialization() {
let info = SignSeparateInfo {
placement: SignPlacement::Leading,
};
let serialized = serde_json::to_string(&info).unwrap();
let deserialized: SignSeparateInfo = serde_json::from_str(&serialized).unwrap();
assert!(matches!(deserialized.placement, SignPlacement::Leading));
}
#[test]
fn test_resolved_renames_serialization() {
let renames = ResolvedRenames {
offset: 10,
length: 50,
members: vec!["FIELD1".to_string(), "FIELD2".to_string()],
};
let serialized = serde_json::to_string(&renames).unwrap();
let deserialized: ResolvedRenames = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.offset, 10);
assert_eq!(deserialized.length, 50);
assert_eq!(deserialized.members.len(), 2);
assert_eq!(deserialized.members[0], "FIELD1");
assert_eq!(deserialized.members[1], "FIELD2");
}
#[test]
fn test_schema_serialization() {
let schema = Schema {
fields: vec![Field::new(1, "ROOT".to_string())],
lrecl_fixed: Some(100),
tail_odo: Some(TailODO {
counter_path: "ROOT.COUNT".to_string(),
min_count: 1,
max_count: 100,
array_path: "ROOT.ARRAY".to_string(),
}),
fingerprint: "test-fingerprint".to_string(),
};
let serialized = serde_json::to_string(&schema).unwrap();
let deserialized: Schema = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.fields.len(), 1);
assert_eq!(deserialized.lrecl_fixed, Some(100));
assert!(deserialized.tail_odo.is_some());
assert_eq!(deserialized.fingerprint, "test-fingerprint");
}
#[test]
fn test_schema_find_field() {
let mut field = Field::new(5, "CHILD".to_string());
field.path = "PARENT.CHILD".to_string();
let schema = Schema {
fields: vec![Field {
path: "PARENT".to_string(),
name: "PARENT".to_string(),
level: 1,
kind: FieldKind::Group,
offset: 0,
len: 10,
redefines_of: None,
occurs: None,
sync_padding: None,
synchronized: false,
blank_when_zero: false,
resolved_renames: None,
children: vec![field],
}],
lrecl_fixed: None,
tail_odo: None,
fingerprint: "test".to_string(),
};
let found = schema.find_field("PARENT.CHILD");
assert!(found.is_some());
assert_eq!(found.unwrap().name, "CHILD");
let not_found = schema.find_field("NONEXISTENT");
assert!(not_found.is_none());
}
#[test]
fn test_schema_find_redefining_fields() {
let _base_field = Field::new(5, "BASE".to_string());
let redef_field1 =
Field::with_kind(5, "REDEF1".to_string(), FieldKind::Alphanum { len: 5 });
let redef_field2 = Field::with_kind(
5,
"REDEF2".to_string(),
FieldKind::ZonedDecimal {
digits: 5,
scale: 0,
signed: false,
sign_separate: None,
},
);
let mut base_field_with_redef = Field::new(5, "BASE".to_string());
base_field_with_redef.path = "ROOT.BASE".to_string();
base_field_with_redef.redefines_of = None;
let mut redef_field1_with_path = redef_field1.clone();
redef_field1_with_path.path = "ROOT.REDEF1".to_string();
redef_field1_with_path.redefines_of = Some("ROOT.BASE".to_string());
let mut redef_field2_with_path = redef_field2.clone();
redef_field2_with_path.path = "ROOT.REDEF2".to_string();
redef_field2_with_path.redefines_of = Some("ROOT.BASE".to_string());
let schema = Schema {
fields: vec![
base_field_with_redef,
redef_field1_with_path,
redef_field2_with_path,
],
lrecl_fixed: None,
tail_odo: None,
fingerprint: "test".to_string(),
};
let redefining = schema.find_redefining_fields("ROOT.BASE");
assert_eq!(redefining.len(), 2);
assert!(redefining.iter().any(|f| f.name == "REDEF1"));
assert!(redefining.iter().any(|f| f.name == "REDEF2"));
}
#[test]
fn test_schema_all_fields() {
let child1 = Field::with_kind(5, "CHILD1".to_string(), FieldKind::Alphanum { len: 5 });
let child2 = Field::with_kind(5, "CHILD2".to_string(), FieldKind::Alphanum { len: 5 });
let mut parent = Field::new(1, "PARENT".to_string());
parent.path = "ROOT.PARENT".to_string();
parent.children = vec![child1, child2];
let top_level = Field::with_kind(1, "TOP".to_string(), FieldKind::Alphanum { len: 10 });
let schema = Schema {
fields: vec![parent, top_level],
lrecl_fixed: None,
tail_odo: None,
fingerprint: "test".to_string(),
};
let all_fields = schema.all_fields();
assert_eq!(all_fields.len(), 4);
assert!(all_fields.iter().any(|f| f.name == "PARENT"));
assert!(all_fields.iter().any(|f| f.name == "CHILD1"));
assert!(all_fields.iter().any(|f| f.name == "CHILD2"));
assert!(all_fields.iter().any(|f| f.name == "TOP"));
}
}