use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use edifact_mapper::{DataDir, Mapper};
use mig_bo4e::code_lists::CodeLists;
use mig_bo4e::definition::{FieldMapping, MappingDefinition};
use mig_bo4e::model::Nachricht;
use serde_json::Value;
const FV: &str = "FV2604";
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("the crate sits two levels below the workspace root")
.to_path_buf()
}
fn toml_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
toml_files(&p, out);
} else if p.extension().is_some_and(|x| x == "toml") {
out.push(p);
}
}
}
fn names_to_codes(root: &Path, variant: &str) -> BTreeMap<String, BTreeMap<String, String>> {
let lists = CodeLists::read(&root.join("mappings/code_lists.toml")).expect("code lists");
let mut files = Vec::new();
toml_files(&root.join("mappings").join(FV).join(variant), &mut files);
let mut seen: BTreeMap<String, BTreeMap<String, BTreeSet<String>>> = BTreeMap::new();
for f in files {
let text = std::fs::read_to_string(&f).expect("read mapping");
let def = MappingDefinition::from_toml_str(&text).expect("parse mapping");
for mapping in def.fields.values() {
let FieldMapping::Structured(s) = mapping else {
continue;
};
let table = match (&s.enum_map, &s.code_list) {
(Some(m), _) => m,
(None, Some(name)) => match lists.get(name) {
Some(m) => m,
None => continue,
},
(None, None) => continue,
};
let Some(key) = s.target.rsplit('.').next().filter(|k| !k.is_empty()) else {
continue;
};
let by_name = seen.entry(key.to_string()).or_default();
for (code, name) in table {
by_name
.entry(name.clone())
.or_default()
.insert(code.clone());
}
}
}
seen.into_iter()
.map(|(key, by_name)| {
let exact = by_name
.into_iter()
.filter(|(_, codes)| codes.len() == 1)
.map(|(name, codes)| (name, codes.into_iter().next().unwrap()))
.collect();
(key, exact)
})
.collect()
}
fn swap(v: &mut Value, tables: &BTreeMap<String, BTreeMap<String, String>>) -> usize {
let mut n = 0;
match v {
Value::Object(o) => {
for (k, child) in o.iter_mut() {
if let Some(table) = tables.get(k) {
let enriched = child.as_object().is_some_and(|e| e.contains_key("meaning"));
let slot = if enriched {
child.get_mut("code")
} else {
Some(&mut *child)
};
if let Some(Value::String(s)) = slot {
if let Some(code) = table.get(s.as_str()) {
*s = code.clone();
n += 1;
continue;
}
}
}
n += swap(child, tables);
}
}
Value::Array(a) => n += a.iter_mut().map(|c| swap(c, tables)).sum::<usize>(),
_ => {}
}
n
}
fn check(variant: &str, msg_dir: &str, pids: &[&str]) {
let root = repo_root();
if !root.join(format!("dist/edifact-data-{FV}.bin")).exists() {
eprintln!("skipping: build the {FV} bundle first");
return;
}
let mapper =
Mapper::from_data_dir(DataDir::path(root.join("dist")).eager(&[FV])).expect("load bundle");
let tables = names_to_codes(&root, variant);
for pid in pids {
let fixture = root.join(format!(
"fixtures/generated/{}/{msg_dir}/{pid}.edi",
FV.to_lowercase()
));
let edifact = std::fs::read_to_string(&fixture)
.unwrap_or_else(|e| panic!("{}: {e}", fixture.display()));
let interchange = mapper
.from_edifact::<Value, Value>(&edifact, FV, variant, pid)
.unwrap_or_else(|e| panic!("{pid}: from_edifact: {e}"));
let named = &interchange.nachrichten[0];
let with_names = mapper
.to_edifact_nachricht(named, FV, variant, pid)
.unwrap_or_else(|e| panic!("{pid}: to_edifact with names: {e}"));
let mut raw = serde_json::to_value(named).expect("serialize");
let swapped = swap(&mut raw, &tables);
assert!(
swapped >= 3,
"{pid}: only {swapped} name(s) swapped for codes — the test no longer exercises anything"
);
let raw: Nachricht<Value, Value> = serde_json::from_value(raw).expect("deserialize");
let with_codes = mapper
.to_edifact_nachricht(&raw, FV, variant, pid)
.unwrap_or_else(|e| panic!("{pid}: to_edifact with raw codes: {e}"));
assert_eq!(
with_codes, with_names,
"{pid}: {swapped} raw code(s) in place of their names rendered a different message"
);
}
}
#[test]
fn utilmd_raw_codes_render_like_their_names() {
check("UTILMD_Strom", "utilmd", &["55001", "55043", "55168"]);
}
#[test]
fn iftsta_raw_codes_render_like_their_names() {
check("IFTSTA", "iftsta", &["21010", "21011"]);
}