use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use edifact_mapper::{DataDir, Mapper};
use mig_bo4e::pid_requirements::PidRequirements;
use mig_bo4e::PidValidationError;
use serde_json::Value;
type Dynamic = mig_bo4e::model::Interchange<Value, Value>;
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("two levels below the workspace root")
.to_path_buf()
}
fn baseline_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/known_nested_list_findings.txt")
}
fn nested_field<'e>(
error: &'e PidValidationError,
requirements: &PidRequirements,
) -> Option<(&'e str, &'e str, &'static str)> {
let (entity, field, kind) = match error {
PidValidationError::MissingField { entity, field, .. } => (entity, field, "missing"),
PidValidationError::InvalidCode { entity, field, .. } => (entity, field, "invalid"),
_ => return None,
};
let (list, _) = field.split_once('[')?;
requirements
.entities
.iter()
.filter(|e| &e.entity == entity)
.any(|e| e.nested.iter().any(|n| n.field == list))
.then_some((entity.as_str(), field.as_str(), kind))
}
fn findings() -> BTreeSet<String> {
let root = repo_root();
let mapper = Mapper::from_data_dir(DataDir::path(root.join("dist")))
.expect("the data bundles are a build artefact — run `bundle-data` first");
let mut out = BTreeSet::new();
let mut fvs: Vec<PathBuf> = std::fs::read_dir(root.join("fixtures/generated"))
.into_iter()
.flatten()
.flatten()
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect();
fvs.sort();
for fv_dir in fvs {
let fv = fv_dir.file_name().unwrap().to_string_lossy().to_uppercase();
let mut msgs: Vec<PathBuf> = std::fs::read_dir(&fv_dir)
.unwrap()
.flatten()
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect();
msgs.sort();
for msg_dir in msgs {
let msg = msg_dir
.file_name()
.unwrap()
.to_string_lossy()
.to_uppercase();
let variants: Vec<String> = if msg == "UTILMD" {
vec![format!("{msg}_Strom"), format!("{msg}_Gas")]
} else {
vec![msg.clone()]
};
let mut files: Vec<PathBuf> = std::fs::read_dir(&msg_dir)
.unwrap()
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|e| e == "edi"))
.collect();
files.sort();
for file in files {
let pid = file.file_stem().unwrap().to_string_lossy().into_owned();
let Ok(edifact) = std::fs::read_to_string(&file) else {
continue;
};
for variant in &variants {
let Ok(ic) = mapper.from_edifact::<Value, Value>(&edifact, &fv, variant, &pid)
else {
continue;
};
let ic: Dynamic = ic;
let requirements = mapper
.pid_requirements(&fv, variant, &pid)
.expect("requirements of a converted PID");
for message in &ic.nachrichten {
for tx in &message.transaktionen {
let mut stammdaten = tx["stammdaten"].clone();
mig_bo4e::model::restore_entity(
&mut stammdaten,
mig_bo4e::model::TX_METADATA_ENTITY,
tx.get("transaktionsdaten").unwrap_or(&Value::Null),
);
for (validator, result) in [
(
"basic",
mapper.validate_pid(&stammdaten, &fv, variant, &pid),
),
(
"conditional",
mapper.validate_pid_with_conditions(
&stammdaten,
&fv,
variant,
&pid,
),
),
] {
for error in result.into_iter().flatten() {
if let Some((entity, field, kind)) =
nested_field(&error, &requirements)
{
out.insert(format!(
"{fv} {variant} {pid} {validator} {kind} {entity}.{field}"
));
}
}
}
}
}
break;
}
}
}
}
out
}
#[test]
fn generated_samples_satisfy_their_nested_list_requirements() {
let found = findings();
if std::env::var("UPDATE_NESTED_LIST_FINDINGS").is_ok() {
let body: String = found.iter().map(|l| format!("{l}\n")).collect();
std::fs::write(baseline_path(), body).expect("write baseline");
eprintln!("wrote {} nested-list finding(s)", found.len());
return;
}
let baseline: BTreeSet<String> = std::fs::read_to_string(baseline_path())
.unwrap_or_default()
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(str::to_owned)
.collect();
let new: Vec<&String> = found.difference(&baseline).collect();
let fixed: Vec<&String> = baseline.difference(&found).collect();
assert!(
new.is_empty(),
"{} new nested-list finding(s) on the generated samples:\n {}",
new.len(),
new.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join("\n ")
);
assert!(
fixed.is_empty(),
"{} listed finding(s) no longer occur; remove them:\n {}",
fixed.len(),
fixed
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join("\n ")
);
}