use std::path::PathBuf;
use edifact_mapper::{DataDir, Mapper};
use serde_json::Value;
const FORMAT_VERSIONS: [&str; 4] = ["FV2504", "FV2510", "FV2604", "FV2610"];
const PID_FEHLERMELDUNG: &str = "92001";
const PID_ANERKENNUNG: &str = "92002";
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.unwrap()
}
fn dist_dir() -> PathBuf {
repo_root().join("dist")
}
fn segments(edifact: &str) -> Vec<String> {
edifact
.split('\'')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
fn tag_of(segment: &str) -> &str {
segment.split(['+', ':']).next().unwrap_or(segment)
}
fn expected_body(edifact: &str) -> String {
segments(edifact)
.into_iter()
.filter(|s| !["UNA", "UNB", "UNH", "UNT", "UNZ"].contains(&tag_of(s)))
.map(|s| format!("{s}'"))
.collect()
}
fn generated_fixture(fv: &str, pid: &str) -> PathBuf {
repo_root()
.join("fixtures/generated")
.join(fv.to_lowercase())
.join("aperak")
.join(format!("{pid}.edi"))
}
fn detection_cases() -> Vec<(String, String, &'static str)> {
let mut out = Vec::new();
for fv in FORMAT_VERSIONS {
for pid in [PID_FEHLERMELDUNG, PID_ANERKENNUNG] {
let path = generated_fixture(fv, pid);
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("{}: {e}", path.display()));
out.push((format!("{fv}/{pid}.edi"), text, pid));
}
}
let corpus = repo_root().join("example_market_communication_bo4e_transactions/APERAK/FV2504");
for (file, expected) in [
("92001_APERAK_2.1i_DEV-99155.edi", PID_FEHLERMELDUNG),
("92001_APERAK_2.1i_DEV-99155-2.edi", PID_FEHLERMELDUNG),
("92001_APERAK_2.1f_JOSCHA60014103.edi", PID_ANERKENNUNG),
] {
if let Ok(text) = std::fs::read_to_string(corpus.join(file)) {
out.push((file.to_string(), text, expected));
}
}
out
}
#[test]
fn detect_pid_tells_the_two_aperak_shapes_apart() {
let dist = dist_dir();
if !dist.join("edifact-data-FV2504.bin").exists() {
eprintln!("skipping: dist/ bundles missing");
return;
}
let mapper = Mapper::from_data_dir(DataDir::path(&dist)).expect("load bundles");
let cases = detection_cases();
assert!(
cases.len() >= 11,
"expected 8 generated fixtures plus the FV2504 corpus, got {}",
cases.len()
);
let mut failures = Vec::new();
for (label, edifact, expected) in cases {
match mapper.detect_pid(&edifact) {
Ok(pid) if pid == expected => {}
Ok(pid) => failures.push(format!("{label}: detected {pid}, expected {expected}")),
Err(e) => failures.push(format!("{label}: detect_pid failed: {e}")),
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn detect_pid_refuses_an_unknown_aperak_document_code() {
let dist = dist_dir();
if !dist.join("edifact-data-FV2504.bin").exists() {
eprintln!("skipping: dist/ bundles missing");
return;
}
let mapper = Mapper::from_data_dir(DataDir::path(&dist)).expect("load bundles");
let edifact = "UNB+UNOC:3+A:500+B:500+250401:1200+REF'\
UNH+MSG+APERAK:D:07B:UN:2.1i'\
BGM+999+MSGBGM'\
UNT+3+MSG'UNZ+1+REF'";
assert!(
mapper.detect_pid(edifact).is_err(),
"an APERAK with BGM+999 must fail loudly, not resolve to 92001 or 92002"
);
}
struct Case {
fv: &'static str,
pid: &'static str,
errors: usize,
}
const CASES: &[Case] = &[
Case {
fv: "FV2504",
pid: PID_FEHLERMELDUNG,
errors: 1,
},
Case {
fv: "FV2504",
pid: PID_ANERKENNUNG,
errors: 0,
},
Case {
fv: "FV2510",
pid: PID_FEHLERMELDUNG,
errors: 1,
},
Case {
fv: "FV2510",
pid: PID_ANERKENNUNG,
errors: 0,
},
Case {
fv: "FV2604",
pid: PID_FEHLERMELDUNG,
errors: 1,
},
Case {
fv: "FV2604",
pid: PID_ANERKENNUNG,
errors: 0,
},
Case {
fv: "FV2610",
pid: PID_FEHLERMELDUNG,
errors: 1,
},
Case {
fv: "FV2610",
pid: PID_ANERKENNUNG,
errors: 0,
},
];
fn check_message_entities(label: &str, msg: &Value, failures: &mut Vec<String>) {
match msg.pointer("/nachricht/nachrichtennummer") {
Some(Value::String(s)) if !s.is_empty() => {}
_ => failures.push(format!(
"{label}: `nachricht.nachrichtennummer` missing; message = {msg}"
)),
}
match msg.pointer("/nachricht/dokumentenCode") {
Some(v) if !v.is_null() => {}
_ => failures.push(format!(
"{label}: `nachricht.dokumentenCode` missing; message = {msg}"
)),
}
if msg.get("marktteilnehmer").is_none() {
failures.push(format!("{label}: no `marktteilnehmer`; message = {msg}"));
}
match msg.get("referenz") {
Some(r) if !r.is_null() => {}
_ => failures.push(format!("{label}: no `referenz`; message = {msg}")),
}
}
#[test]
fn aperak_maps_to_entities_and_back_for_both_pids() {
let dist = dist_dir();
if !dist.join("edifact-data-FV2504.bin").exists() {
eprintln!("skipping: dist/ bundles missing");
return;
}
let mut failures: Vec<String> = Vec::new();
for case in CASES {
let label = format!("{}/{}", case.fv, case.pid);
let path = generated_fixture(case.fv, case.pid);
let Ok(edifact) = std::fs::read_to_string(&path) else {
failures.push(format!("{label}: fixture missing at {}", path.display()));
continue;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&dist).eager(&[case.fv])).expect("load bundle");
let ic = match mapper.from_edifact::<Value, Value>(&edifact, case.fv, "APERAK", case.pid) {
Ok(ic) => ic,
Err(e) => {
failures.push(format!("{label}: from_edifact failed: {e}"));
continue;
}
};
let nachricht = &ic.nachrichten[0];
let mut msg = nachricht.stammdaten.clone();
mig_bo4e::model::restore_message_metadata(&mut msg, &nachricht.nachrichtendaten);
check_message_entities(&label, &msg, &mut failures);
if nachricht.transaktionen.len() != case.errors {
failures.push(format!(
"{label}: expected {} error transaction(s), got {}: {:?}",
case.errors,
nachricht.transaktionen.len(),
nachricht.transaktionen
));
} else {
for (i, tx) in nachricht.transaktionen.iter().enumerate() {
let stamm = tx.get("stammdaten").unwrap_or(tx);
let Some(fehler) = stamm.get("fehler") else {
failures.push(format!("{label}: transaction {i} has no `fehler`: {stamm}"));
continue;
};
if fehler.get("fehlerCode").is_none() {
failures.push(format!(
"{label}: transaction {i} `fehler.fehlerCode` missing: {fehler}"
));
}
}
}
match mapper.to_edifact(&msg, &nachricht.transaktionen, case.fv, "APERAK", case.pid) {
Ok(rendered) => {
let want = expected_body(&edifact);
if rendered != want {
failures.push(format!(
"{label}: reverse is not byte-identical\n want: {want}\n got: {rendered}"
));
}
}
Err(e) => failures.push(format!("{label}: to_edifact failed: {e}")),
}
}
assert!(
failures.is_empty(),
"APERAK is not fully reachable through `Mapper`:\n{}",
failures.join("\n")
);
}
#[test]
fn aperak_corpus_roundtrips_through_the_mapper() {
let dist = dist_dir();
if !dist.join("edifact-data-FV2504.bin").exists() {
eprintln!("skipping: dist/ bundles missing");
return;
}
let corpus = repo_root().join("example_market_communication_bo4e_transactions/APERAK/FV2504");
if !corpus.is_dir() {
eprintln!("skipping: APERAK corpus not checked out");
return;
}
let mapper = Mapper::from_data_dir(DataDir::path(&dist).eager(&["FV2504"])).expect("bundle");
let mut files: Vec<PathBuf> = std::fs::read_dir(&corpus)
.unwrap()
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|e| e == "edi"))
.collect();
files.sort();
assert!(!files.is_empty(), "APERAK FV2504 corpus is empty");
let mut failures = Vec::new();
for file in files {
let label = file.file_name().unwrap().to_string_lossy().into_owned();
let edifact = std::fs::read_to_string(&file).unwrap();
let pid = match mapper.detect_pid(&edifact) {
Ok(p) => p,
Err(e) => {
failures.push(format!("{label}: detect_pid failed: {e}"));
continue;
}
};
let ic = match mapper.from_edifact::<Value, Value>(&edifact, "FV2504", "APERAK", &pid) {
Ok(ic) => ic,
Err(e) => {
failures.push(format!("{label} ({pid}): from_edifact failed: {e}"));
continue;
}
};
let nachricht = &ic.nachrichten[0];
let mut msg = nachricht.stammdaten.clone();
mig_bo4e::model::restore_message_metadata(&mut msg, &nachricht.nachrichtendaten);
check_message_entities(&format!("{label} ({pid})"), &msg, &mut failures);
match mapper.to_edifact(&msg, &nachricht.transaktionen, "FV2504", "APERAK", &pid) {
Ok(rendered) => {
let want = expected_body(&edifact);
if rendered != want {
failures.push(format!(
"{label} ({pid}): reverse is not byte-identical\n want: {want}\n got: {rendered}"
));
}
}
Err(e) => failures.push(format!("{label} ({pid}): to_edifact failed: {e}")),
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn aperak_bundles_carry_exactly_two_pids() {
let dist = dist_dir();
let expected: Vec<String> = vec![
format!("pid_{PID_FEHLERMELDUNG}"),
format!("pid_{PID_ANERKENNUNG}"),
];
let mut failures = Vec::new();
for fv in FORMAT_VERSIONS {
let path = dist.join(format!("edifact-data-{fv}.bin"));
if !path.exists() {
eprintln!("skipping {fv}: bundle missing");
continue;
}
let bundle = mig_bo4e::engine::DataBundle::load(&path).expect("load bundle");
let Some(vc) = bundle.variant("APERAK") else {
failures.push(format!("{fv}: no APERAK variant in bundle"));
continue;
};
if vc.message_defs.is_empty() {
failures.push(format!("{fv}: APERAK message_defs is empty"));
}
let mut pids: Vec<String> = vc.transaction_defs.keys().cloned().collect();
pids.sort();
let mut want = expected.clone();
want.sort();
if pids != want {
failures.push(format!("{fv}: APERAK PIDs are {pids:?}, expected {want:?}"));
}
match vc.transaction_defs.get(&format!("pid_{PID_FEHLERMELDUNG}")) {
Some(defs) if !defs.is_empty() => {}
other => failures.push(format!(
"{fv}: APERAK {PID_FEHLERMELDUNG} must map SG4, got {:?}",
other.map(Vec::len)
)),
}
for pid in [PID_FEHLERMELDUNG, PID_ANERKENNUNG] {
let key = format!("pid_{pid}");
match vc.pid_segment_numbers.get(&key) {
Some(numbers) if !numbers.is_empty() => {}
other => failures.push(format!(
"{fv}: APERAK {key} carries no AHB segment numbers: {other:?}"
)),
}
if !vc.pid_requirements.contains_key(&key) {
failures.push(format!("{fv}: APERAK {key} has no PID requirements"));
}
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}