use std::path::PathBuf;
use edifact_mapper::{DataDir, Mapper};
use mig_bo4e::PidValidationError;
use serde_json::Value;
type Dynamic = mig_bo4e::model::Interchange<Value, Value>;
const VARIANT: &str = "UTILMD_Strom";
const PID: &str = "55042";
const PARTNERROLLE: &[(&str, &str)] = &[
("Z03", "messlokationsadresse"),
("Z05", "ablesekarte"),
("Z07", "kundeMsb"),
("Z08", "korrespondenzKundeMsb"),
];
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("crate is two levels under the workspace root")
.to_path_buf()
}
fn load(fv: &str) -> Option<(Mapper, Value)> {
let root = repo_root();
let fixture = root.join(format!(
"fixtures/generated/{}/utilmd/{PID}.edi",
fv.to_lowercase()
));
if !root.join(format!("dist/edifact-data-{fv}.bin")).exists() || !fixture.exists() {
eprintln!("skipping: regenerate the {fv} bundle + fixture first");
return None;
}
let mapper = Mapper::from_data_dir(DataDir::path(root.join("dist"))).expect("load bundle");
let edifact = std::fs::read_to_string(&fixture).expect("read fixture");
let ic: Dynamic = mapper
.from_edifact(&edifact, fv, VARIANT, PID)
.expect("convert fixture");
let raw = ic.nachrichten[0].transaktionen[0].clone();
let tx = match raw.get("stammdaten") {
Some(st) => {
let mut out = st.clone();
mig_bo4e::model::restore_entity(
&mut out,
mig_bo4e::model::TX_METADATA_ENTITY,
raw.get("transaktionsdaten").unwrap_or(&Value::Null),
);
out
}
None => raw,
};
let gp = tx
.get("geschaeftspartner")
.and_then(Value::as_array)
.expect("55042 fixture has a Geschaeftspartner array");
assert_eq!(gp.len(), 4, "one Geschaeftspartner per SG12 variant");
Some((mapper, tx))
}
fn map_partnerrolle(tx: &Value, f: impl Fn(&Value) -> Value) -> Value {
let mut tx = tx.clone();
for gp in tx["geschaeftspartner"].as_array_mut().unwrap() {
let new = f(&gp["partnerrolle"]);
gp["partnerrolle"] = new;
}
tx
}
fn code_of(v: &Value) -> String {
v.get("code")
.and_then(Value::as_str)
.or_else(|| v.as_str())
.expect("partnerrolle is a code object or string")
.to_string()
}
fn raw_code(name: &str) -> &'static str {
PARTNERROLLE
.iter()
.find(|(_, n)| *n == name)
.map(|(c, _)| *c)
.unwrap_or_else(|| panic!("unexpected partnerrolle {name}"))
}
fn validate_both(mapper: &Mapper, fv: &str, tx: &Value) -> Vec<(&'static str, PidValidationError)> {
let mut out = Vec::new();
for e in mapper.validate_pid(tx, fv, VARIANT, PID).unwrap() {
out.push(("validate_pid", e));
}
for e in mapper
.validate_pid_with_conditions(tx, fv, VARIANT, PID)
.unwrap()
{
out.push(("validate_pid_with_conditions", e));
}
out
}
fn geschaeftspartner_false_positives(errors: &[(&'static str, PidValidationError)]) -> Vec<String> {
errors
.iter()
.filter(|(_, e)| match e {
PidValidationError::InvalidCode { entity, .. } => entity == "Geschaeftspartner",
PidValidationError::MissingField { entity, .. } => {
entity == "Geschaeftspartner" && e.is_error()
}
_ => false,
})
.map(|(which, e)| format!("[{which}] {e}"))
.collect()
}
fn assert_valid_forms(fv: &str) {
let Some((mapper, tx)) = load(fv) else {
return;
};
let forms = [
("parser output (code objects)", tx.clone()),
(
"partnerrolle as mapped-name strings",
map_partnerrolle(&tx, |v| Value::String(code_of(v))),
),
(
"partnerrolle as raw EDIFACT code strings",
map_partnerrolle(&tx, |v| Value::String(raw_code(&code_of(v)).to_string())),
),
(
"partnerrolle as raw EDIFACT code objects",
map_partnerrolle(
&tx,
|v| serde_json::json!({ "code": raw_code(&code_of(v)) }),
),
),
];
for (label, json) in forms {
let errors = validate_both(&mapper, fv, &json);
let bad = geschaeftspartner_false_positives(&errors);
assert!(
bad.is_empty(),
"{fv} {label}: valid 55042 Geschaeftspartner reported as invalid:\n{}",
bad.join("\n")
);
}
}
fn assert_invalid_code_reported(fv: &str) {
let Some((mapper, tx)) = load(fv) else {
return;
};
let forms = [
("plain string", Value::String("bogus".to_string())),
("raw-looking string", Value::String("Z99".to_string())),
("code object", serde_json::json!({ "code": "Z99" })),
];
for (label, bad_value) in forms {
let json = map_partnerrolle(&tx, |v| {
if code_of(v) == "kundeMsb" {
bad_value.clone()
} else {
v.clone()
}
});
let errors = validate_both(&mapper, fv, &json);
for which in ["validate_pid", "validate_pid_with_conditions"] {
let invalid: Vec<_> = errors
.iter()
.filter(|(w, e)| {
*w == which
&& matches!(e, PidValidationError::InvalidCode { entity, field, .. }
if entity == "Geschaeftspartner" && field == "partnerrolle")
})
.collect();
assert_eq!(
invalid.len(),
1,
"{fv} {which} {label}: expected exactly one InvalidCode for the bogus \
partnerrolle, got: {errors:#?}"
);
let PidValidationError::InvalidCode { valid_values, .. } = &invalid[0].1 else {
unreachable!()
};
for (code, _) in PARTNERROLLE {
assert!(
valid_values.iter().any(|(c, _)| c == code),
"{fv} {which}: valid_values must list every SG12 variant's code, \
missing {code}: {valid_values:?}"
);
}
}
}
}
#[test]
fn fv2604_55042_geschaeftspartner_valid_forms_pass() {
assert_valid_forms("FV2604");
}
#[test]
fn fv2610_55042_geschaeftspartner_valid_forms_pass() {
assert_valid_forms("FV2610");
}
#[test]
fn fv2604_55042_geschaeftspartner_invalid_code_reported() {
assert_invalid_code_reported("FV2604");
}
#[test]
fn fv2610_55042_geschaeftspartner_invalid_code_reported() {
assert_invalid_code_reported("FV2610");
}