use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
const NOT_MODELLED: &[(&str, &str)] = &[
(
"TimeIntervalError",
"per-interval syntax error detail; mako rejects the whole Übertragungsdatei \
(FB 1.0g: an ACK confirms or rejects the file as a whole), so the interval \
breakdown has no consumer",
),
(
"QuantityTimeInterval",
"child of TimeIntervalError, which is excused above for the same reason",
),
(
"SendersTimeSeriesIdentification",
"child of TimeSeriesRejection, which mako does not emit for the same reason",
),
(
"CR_Objekt",
"Cluster objects — the clusternder NB's own Stammdaten. mako holds the ANB \
and BKV sides",
),
(
"SG_Objekt",
"Steuergruppen — SR that share one Steuersignal (BilAReM Kap. 1). Same \
role boundary as CR_Objekt: mako holds the ANB and BKV sides",
),
(
"CR_Objekt_Referenz",
"child of Enthaltene_Objektreferenzen, only reachable from CR_Objekt/SG_Objekt",
),
(
"SG_Objekt_Referenz",
"child of Enthaltene_Objektreferenzen, only reachable from CR_Objekt/SG_Objekt",
),
(
"Enthaltene_Objektreferenzen",
"container inside CR_Objekt/SG_Objekt",
),
(
"Clusternder_Netzbetreiber",
"the NB that formed the Cluster — a CR_Objekt field, unreachable until \
CR_Objekt itself is modelled (BilAReM Kap. 6.2.1.7 makes it the owner of \
all clusterbezogene Stammdaten)",
),
(
"tx_Cluster",
"the Cluster's own dispatch lead time — a CR_Objekt field, unreachable \
until CR_Objekt itself is modelled",
),
(
"T_Abruf_final",
"the point at which an Abruf becomes final — a CR_Objekt and SG_Objekt \
field, unreachable until those objects are modelled",
),
];
fn xsd_dir() -> Option<PathBuf> {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()?
.parent()?
.join("regulatories/bdew-mako");
root.is_dir().then_some(root)
}
fn newest_xsds(dir: &Path) -> Vec<(String, PathBuf)> {
let mut best: std::collections::BTreeMap<String, PathBuf> = std::collections::BTreeMap::new();
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("xsd") {
continue;
}
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();
let key = name.split(" XSD").next().unwrap_or(name).to_owned();
let better = name.contains("Fehlerkorrektur");
match best.get(&key) {
Some(_) if !better => {}
_ => {
best.insert(key, path);
}
}
}
best.into_iter().collect()
}
fn declared_elements(xsd: &str) -> BTreeSet<String> {
let mut out = BTreeSet::new();
let needle = "<xs:element name=\"";
let mut rest = xsd;
while let Some(i) = rest.find(needle) {
rest = &rest[i + needle.len()..];
if let Some(end) = rest.find('"') {
out.insert(rest[..end].to_owned());
}
}
out
}
fn module_for(doc: &str) -> &'static str {
match doc {
"AcknowledgementDocument" => "acknowledgement.rs",
"ActivationDocument" => "activation.rs",
"Kaskade" => "kaskade.rs",
"Kostenblatt" => "kostenblatt.rs",
"NetworkConstraintDocument" => "network_constraint.rs",
"PlannedResourceScheduleDocument" => "planned_resource_schedule.rs",
"Stammdaten" => "stammdaten.rs",
"StatusRequest_MarketDocument" => "status_request.rs",
"Unavailability_MarketDocument" => "unavailability.rs",
other => panic!("no module mapped for XSD {other}"),
}
}
fn source_for(doc: &str) -> String {
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut buf = String::new();
if let Ok(text) = std::fs::read_to_string(src.join("documents").join(module_for(doc))) {
buf.push_str(&text);
}
if let Ok(entries) = std::fs::read_dir(src.join("types")) {
for entry in entries.flatten() {
if entry.path().extension().and_then(|e| e.to_str()) == Some("rs")
&& let Ok(text) = std::fs::read_to_string(entry.path())
{
buf.push_str(&text);
}
}
}
buf
}
#[test]
fn every_xsd_element_is_modelled_or_explained() {
let Some(dir) = xsd_dir() else {
eprintln!(
"SKIP xsd_coverage: regulatories/bdew-mako/ is absent. The XSDs are \
third-party publications and are not tracked in git; \
regulatories/README.md carries the download URL for each one."
);
return;
};
let xsds = newest_xsds(&dir);
assert!(
!xsds.is_empty(),
"regulatories/bdew-mako/ exists but holds no .xsd files"
);
let excused: BTreeSet<&str> = NOT_MODELLED.iter().map(|(name, _)| *name).collect();
let mut missing: Vec<(String, String)> = Vec::new();
let mut checked = 0usize;
for (doc, path) in &xsds {
let Ok(xsd) = std::fs::read_to_string(path) else {
continue;
};
let source = source_for(doc);
for element in declared_elements(&xsd) {
checked += 1;
if excused.contains(element.as_str()) {
continue;
}
let renamed = source.contains(&format!("\"{element}\""));
let bare_field = source.contains(&format!("pub {element}:"));
if !renamed && !bare_field {
missing.push((doc.clone(), element));
}
}
}
assert!(
!missing.is_empty() || checked > 0,
"no elements were checked — the XSD parser found nothing"
);
assert!(
missing.is_empty(),
"these XSD elements are declared by BDEW but appear nowhere in the model, so an \
inbound document carrying one is parsed with the value silently dropped:\n{}\n\n\
Either model the element, or add it to NOT_MODELLED with the reason.",
missing
.iter()
.map(|(doc, el)| format!(" {doc}: {el}"))
.collect::<Vec<_>>()
.join("\n")
);
}
#[test]
fn every_not_modelled_entry_carries_a_reason() {
for (name, reason) in NOT_MODELLED {
assert!(
reason.len() > 20,
"NOT_MODELLED entry {name:?} needs a real reason, not {reason:?}"
);
}
let mut names: Vec<&str> = NOT_MODELLED.iter().map(|(n, _)| *n).collect();
let total = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), total, "duplicate entry in NOT_MODELLED");
}