use crate::generate::{XmlElement, XmlNode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CobolValue {
Alphanumeric(Vec<u8>),
Numeric { digits: Vec<u8>, scale: usize, negative: bool },
Group(Vec<CobolField>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CobolField {
pub name: String,
pub value: CobolValue,
}
impl CobolField {
pub fn alnum(name: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
CobolField { name: name.into(), value: CobolValue::Alphanumeric(data.into()) }
}
pub fn numeric(name: impl Into<String>, digits: impl Into<Vec<u8>>, scale: usize, negative: bool) -> Self {
CobolField { name: name.into(), value: CobolValue::Numeric { digits: digits.into(), scale, negative } }
}
pub fn group(name: impl Into<String>, children: Vec<CobolField>) -> Self {
CobolField { name: name.into(), value: CobolValue::Group(children) }
}
}
pub fn sanitize_name(name: &str) -> String {
if name.is_empty() {
return "_".to_string();
}
let mut out = String::new();
if name.chars().next().unwrap().is_ascii_digit() {
out.push('_');
}
out.push_str(name);
out
}
fn render_alnum(data: &[u8]) -> String {
let mut end = data.len();
while end > 0 && (data[end - 1] == b' ' || data[end - 1] == 0) {
end -= 1;
}
String::from_utf8_lossy(&data[..end]).into_owned()
}
fn render_numeric(digits: &[u8], scale: usize, negative: bool) -> String {
let mut d: Vec<u8> = digits.iter().copied().filter(|b| b.is_ascii_digit()).collect();
if d.is_empty() {
d.push(b'0');
}
let int_len = d.len().saturating_sub(scale);
let int_part = &d[..int_len];
let mut start = 0;
while start + 1 < int_part.len() && int_part[start] == b'0' {
start += 1;
}
let mut s = String::new();
if negative {
s.push('-');
}
if int_part.is_empty() {
s.push('0');
} else {
s.push_str(&String::from_utf8_lossy(&int_part[start..]));
}
if scale > 0 {
s.push('.');
s.push_str(&String::from_utf8_lossy(&d[int_len..]));
}
s
}
fn field_to_element(f: &CobolField) -> Option<XmlElement> {
if f.name.eq_ignore_ascii_case("FILLER") {
return None;
}
let name = sanitize_name(&f.name);
let el = match &f.value {
CobolValue::Alphanumeric(d) => XmlElement::leaf(name, render_alnum(d)),
CobolValue::Numeric { digits, scale, negative } => XmlElement::leaf(name, render_numeric(digits, *scale, *negative)),
CobolValue::Group(children) => {
let kids: Vec<XmlNode> = children.iter().filter_map(field_to_element).map(XmlNode::Element).collect();
XmlElement::group(name, kids)
}
};
Some(el)
}
pub fn record_to_xml(root: &CobolField) -> XmlElement {
field_to_element(root).unwrap_or_else(|| XmlElement::empty(sanitize_name(&root.name)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generate::{generate, GenerateOptions};
#[test]
fn sanitize_names() {
assert_eq!(sanitize_name("CUST-NAME"), "CUST-NAME");
assert_eq!(sanitize_name("1ST"), "_1ST");
assert_eq!(sanitize_name(""), "_");
}
#[test]
fn numeric_rendering() {
assert_eq!(render_numeric(b"042", 0, false), "42");
assert_eq!(render_numeric(b"01250", 2, false), "12.50");
assert_eq!(render_numeric(b"0000", 0, false), "0");
assert_eq!(render_numeric(b"042", 0, true), "-42");
}
#[test]
fn record_to_xml_full() {
let rec = CobolField::group(
"G",
vec![
CobolField::alnum("NAME", &b"JOHN"[..]),
CobolField::numeric("AMT", &b"01250"[..], 2, false),
CobolField::group("GRP", vec![CobolField::alnum("X", &b"hi"[..])]),
CobolField::alnum("FILLER", &b" "[..]),
CobolField::alnum("MSG", &b"a<b&c "[..]),
],
);
let xml = generate(&record_to_xml(&rec), &GenerateOptions::default());
assert_eq!(
xml,
"<G><NAME>JOHN</NAME><AMT>12.50</AMT><GRP><X>hi</X></GRP><MSG>a<b&c</MSG></G>"
);
}
}