use std::path::PathBuf;
use edifact_mapper::{DataDir, Mapper};
use mig_assembly::pid_detect::CONTRL_PID;
use serde_json::Value;
const FORMAT_VERSIONS: [&str; 4] = ["FV2504", "FV2510", "FV2604", "FV2610"];
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 message(body: &[&str]) -> String {
let mut segs: Vec<String> = vec![
"UNB+UNOC:3+9903111000003:500+9900269000000:500+220614:1519+X315521".to_string(),
"UNH+X315521+CONTRL:D:3:UN:2.0b".to_string(),
];
segs.extend(body.iter().map(|s| (*s).to_string()));
segs.push(format!("UNT+{}+X315521", body.len() + 2));
segs.push("UNZ+1+X315521".to_string());
segs.iter().map(|s| format!("{s}'")).collect()
}
fn empfangsbestaetigung() -> String {
message(&["UCI+10000000172UCI+9903111000003:500+9900269000000:500+7"])
}
fn interchange_error() -> String {
message(&["UCI+10000000172UCI+9903111000003:500+9900269000000:500+4+13+UNB+2:3"])
}
fn detection_cases() -> Vec<(String, String)> {
let corpus = repo_root().join("example_market_communication_bo4e_transactions/CONTRL/FV2210");
let mut out = Vec::new();
for file in [
"91001_CONTRL_2.0b_GPKELF18197957_pos.edi",
"91001_CONTRL_2.0b_X3155219999460_neg.edi",
] {
if let Ok(text) = std::fs::read_to_string(corpus.join(file)) {
out.push((file.to_string(), text));
}
}
out.push((
"synthetic interchange-level syntax error".to_string(),
interchange_error(),
));
out
}
#[test]
fn every_contrl_reports_pid_91001() {
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() >= 3,
"expected the CONTRL corpus fixtures to be checked out"
);
let mut failures = Vec::new();
for (label, edifact) in cases {
match mapper.detect_pid(&edifact) {
Ok(pid) if pid == CONTRL_PID => {}
Ok(pid) => failures.push(format!("{label}: detected {pid}, expected {CONTRL_PID}")),
Err(e) => failures.push(format!("{label}: detect_pid failed: {e}")),
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[derive(Clone, Copy)]
enum Shape {
GeneratedFixture,
Empfangsbestaetigung,
InterchangeError,
}
struct Case {
fv: &'static str,
shape: Shape,
checked_messages: usize,
}
const CASES: &[Case] = &[
Case {
fv: "FV2504",
shape: Shape::GeneratedFixture,
checked_messages: 1,
},
Case {
fv: "FV2510",
shape: Shape::GeneratedFixture,
checked_messages: 1,
},
Case {
fv: "FV2604",
shape: Shape::GeneratedFixture,
checked_messages: 1,
},
Case {
fv: "FV2610",
shape: Shape::GeneratedFixture,
checked_messages: 1,
},
Case {
fv: "FV2504",
shape: Shape::Empfangsbestaetigung,
checked_messages: 0,
},
Case {
fv: "FV2610",
shape: Shape::Empfangsbestaetigung,
checked_messages: 0,
},
Case {
fv: "FV2504",
shape: Shape::InterchangeError,
checked_messages: 0,
},
Case {
fv: "FV2610",
shape: Shape::InterchangeError,
checked_messages: 0,
},
];
fn label_of(case: &Case) -> String {
let shape = match case.shape {
Shape::GeneratedFixture => "generated fixture",
Shape::Empfangsbestaetigung => "Empfangsbestätigung (UCI only)",
Shape::InterchangeError => "interchange-level syntax error",
};
format!("{}/{shape}", case.fv)
}
fn edifact_for(case: &Case) -> Option<String> {
match case.shape {
Shape::GeneratedFixture => {
let path = repo_root()
.join("fixtures/generated")
.join(case.fv.to_lowercase())
.join("contrl/91001.edi");
std::fs::read_to_string(path).ok()
}
Shape::Empfangsbestaetigung => Some(empfangsbestaetigung()),
Shape::InterchangeError => Some(interchange_error()),
}
}
#[test]
fn contrl_maps_to_entities_and_back() {
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 = label_of(case);
let Some(edifact) = edifact_for(case) else {
failures.push(format!("{label}: fixture missing"));
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, "CONTRL", CONTRL_PID)
{
Ok(ic) => ic,
Err(e) => {
failures.push(format!("{label}: from_edifact failed: {e}"));
continue;
}
};
let nachricht = &ic.nachrichten[0];
let pruefung = nachricht.stammdaten.get("uebertragungspruefung");
match pruefung.and_then(|p| p.get("datenaustauschreferenz")) {
Some(Value::String(r)) if !r.is_empty() => {}
_ => failures.push(format!(
"{label}: `uebertragungspruefung.datenaustauschreferenz` missing; stammdaten = {}",
nachricht.stammdaten
)),
}
if nachricht.transaktionen.len() != case.checked_messages {
failures.push(format!(
"{label}: expected {} transaction(s), got {}: {:?}",
case.checked_messages,
nachricht.transaktionen.len(),
nachricht.transaktionen
));
} else {
for (i, tx) in nachricht.transaktionen.iter().enumerate() {
let stamm = tx.get("stammdaten").unwrap_or(tx);
let Some(np) = stamm.get("nachrichtenpruefung") else {
failures.push(format!(
"{label}: transaction {i} has no `nachrichtenpruefung`: {stamm}"
));
continue;
};
if np
.get("nachrichtenReferenznummer")
.and_then(Value::as_str)
.is_none()
{
failures.push(format!(
"{label}: transaction {i} `nachrichtenpruefung.nachrichtenReferenznummer` missing: {np}"
));
}
let errors = np.get("segmentfehler").and_then(Value::as_array);
if errors.is_none_or(Vec::is_empty) {
failures.push(format!(
"{label}: transaction {i} carries no nested `segmentfehler`: {np}"
));
}
}
}
let mut msg = nachricht.stammdaten.clone();
mig_bo4e::model::restore_message_metadata(&mut msg, &nachricht.nachrichtendaten);
match mapper.to_edifact(
&msg,
&nachricht.transaktionen,
case.fv,
"CONTRL",
CONTRL_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(),
"CONTRL is not reachable through `Mapper`:\n{}",
failures.join("\n")
);
}
#[test]
fn contrl_bundles_carry_exactly_one_pid() {
let dist = dist_dir();
let expected_key = format!("pid_{CONTRL_PID}");
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("CONTRL") else {
failures.push(format!("{fv}: no CONTRL variant in bundle"));
continue;
};
if vc.message_defs.is_empty() {
failures.push(format!("{fv}: CONTRL message_defs is empty"));
}
let pids: Vec<&String> = vc.transaction_defs.keys().collect();
if pids != vec![&expected_key] {
failures.push(format!(
"{fv}: CONTRL PIDs are {pids:?}, expected only [{expected_key}]"
));
}
match vc.transaction_defs.get(&expected_key) {
Some(defs) if defs.len() >= 2 => {}
other => failures.push(format!(
"{fv}: CONTRL {expected_key} must map SG1 and SG1.SG2, got {:?}",
other.map(Vec::len)
)),
}
match vc.pid_segment_numbers.get(&expected_key) {
Some(numbers) if numbers.len() == 6 => {}
other => failures.push(format!(
"{fv}: CONTRL {expected_key} must carry all six segment numbers, got {other:?}"
)),
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}