#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldKind {
Alphanumeric,
Numeric { scale: usize, signed: bool },
Group(Vec<FieldDecl>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldDecl {
pub name: String,
pub pic: String,
pub offset: usize,
pub length: usize,
pub kind: FieldKind,
}
impl FieldDecl {
pub fn alnum(name: impl Into<String>, pic: impl Into<String>, offset: usize, length: usize) -> Self {
FieldDecl { name: name.into(), pic: pic.into(), offset, length, kind: FieldKind::Alphanumeric }
}
pub fn numeric(
name: impl Into<String>,
pic: impl Into<String>,
offset: usize,
length: usize,
scale: usize,
signed: bool,
) -> Self {
FieldDecl {
name: name.into(),
pic: pic.into(),
offset,
length,
kind: FieldKind::Numeric { scale, signed },
}
}
pub fn group(name: impl Into<String>, offset: usize, children: Vec<FieldDecl>) -> Self {
let length = children.iter().map(|c| c.length).sum();
FieldDecl { name: name.into(), pic: String::new(), offset, length, kind: FieldKind::Group(children) }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Copybook {
pub record_name: String,
pub encoding: String,
pub fields: Vec<FieldDecl>,
}
impl Copybook {
pub fn record_length(&self) -> usize {
self.fields.iter().map(|f| f.length).sum()
}
pub fn canonical_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(b"COPYBOOK\x1f");
out.extend_from_slice(self.record_name.as_bytes());
out.push(0x1f);
out.extend_from_slice(self.encoding.as_bytes());
out.push(0x1e);
for f in &self.fields {
canon_field(f, &mut out);
}
out
}
}
fn canon_field(f: &FieldDecl, out: &mut Vec<u8>) {
out.extend_from_slice(f.name.as_bytes());
out.push(0x1f);
out.extend_from_slice(f.pic.as_bytes());
out.push(0x1f);
out.extend_from_slice(f.offset.to_string().as_bytes());
out.push(0x1f);
out.extend_from_slice(f.length.to_string().as_bytes());
out.push(0x1f);
match &f.kind {
FieldKind::Alphanumeric => out.extend_from_slice(b"A"),
FieldKind::Numeric { scale, signed } => {
out.extend_from_slice(b"N");
out.extend_from_slice(scale.to_string().as_bytes());
out.push(if *signed { b'S' } else { b'U' });
}
FieldKind::Group(children) => {
out.extend_from_slice(b"G{");
for c in children {
canon_field(c, out);
}
out.push(b'}');
}
}
out.push(0x1e);
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finding {
pub code: String,
pub message: String,
}
impl Finding {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Finding { code: code.into(), message: message.into() }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedField {
pub name: String,
pub value: String,
pub raw: Vec<u8>,
pub decl: FieldDecl,
pub findings: Vec<Finding>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_length_sums_children() {
let cb = Copybook {
record_name: "REC".into(),
encoding: "ascii".into(),
fields: vec![
FieldDecl::alnum("NAME", "X(4)", 0, 4),
FieldDecl::numeric("AMT", "9(3)V99", 4, 5, 2, false),
],
};
assert_eq!(cb.record_length(), 9);
}
#[test]
fn group_length_is_sum() {
let g = FieldDecl::group(
"G",
0,
vec![FieldDecl::alnum("A", "X(2)", 0, 2), FieldDecl::alnum("B", "X(3)", 2, 3)],
);
assert_eq!(g.length, 5);
}
#[test]
fn canonical_bytes_deterministic() {
let cb = Copybook {
record_name: "REC".into(),
encoding: "ascii".into(),
fields: vec![FieldDecl::alnum("NAME", "X(4)", 0, 4)],
};
assert_eq!(cb.canonical_bytes(), cb.canonical_bytes());
assert!(!cb.canonical_bytes().is_empty());
}
}