#![cfg(feature = "versioned")]
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
const SERIES: &str = "v202607";
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn schema_dir() -> PathBuf {
let root = workspace_root().join("generator/schemas");
let mut matches: Vec<PathBuf> = std::fs::read_dir(&root)
.unwrap_or_else(|e| panic!("reading {}: {e}", root.display()))
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| {
p.is_dir()
&& p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with(SERIES))
})
.collect();
matches.sort();
match matches.len() {
1 => matches.pop().expect("checked len"),
0 => panic!(
"no committed schema snapshot for series {SERIES} under {}",
root.display()
),
n => panic!(
"{n} schema snapshots for series {SERIES} under {} — commit exactly one: {matches:?}",
root.display()
),
}
}
fn schema_version() -> String {
schema_dir()
.file_name()
.and_then(|n| n.to_str())
.expect("schema dir name is UTF-8")
.to_owned()
}
fn generated_dir() -> PathBuf {
workspace_root().join("src/generated").join(SERIES)
}
fn read_schemas(dir: &Path) -> Vec<(String, serde_json::Value)> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut paths: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "json"))
.collect();
paths.sort();
paths
.into_iter()
.map(|p| {
let raw = std::fs::read_to_string(&p)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", p.display()));
let doc: serde_json::Value = serde_json::from_str(&raw)
.unwrap_or_else(|e| panic!("invalid JSON in {}: {e}", p.display()));
let title = doc["title"]
.as_str()
.unwrap_or_else(|| panic!("{} has no title", p.display()))
.to_owned();
(title, doc)
})
.collect()
}
fn generated_source_flat(title: &str) -> String {
generated_source(title).split_whitespace().collect()
}
fn generated_source(title: &str) -> String {
let file = generated_dir().join(format!("{}.rs", heck::AsSnakeCase(title)));
std::fs::read_to_string(&file).unwrap_or_else(|e| {
panic!(
"no generated file for schema {title:?} at {} ({e}) — run `just generate`",
file.display()
)
})
}
fn metadata_literal(doc: &serde_json::Value, key: &str) -> Option<String> {
let prop = doc.get("properties")?.get(key)?;
prop.get("const")
.or_else(|| prop.get("default"))
.and_then(|v| v.as_str())
.map(str::to_owned)
}
#[test]
fn every_schema_has_generated_code_and_vice_versa() {
let mut expected: Vec<String> = Vec::new();
for category in ["bo", "com", "enum"] {
for (title, _) in read_schemas(&schema_dir().join(category)) {
expected.push(format!("{}.rs", heck::AsSnakeCase(&title)));
}
}
for (title, _) in read_schemas(&schema_dir()) {
expected.push(format!("{}.rs", heck::AsSnakeCase(&title)));
}
expected.sort();
assert!(
expected.len() >= 150,
"expected the full BO4E schema set, found only {} files under {} — \
run `just download-schemas {}`",
expected.len(),
schema_dir().display(),
schema_version(),
);
let mut actual: Vec<String> = std::fs::read_dir(generated_dir())
.expect("src/generated/v202607 must exist — run `just generate`")
.filter_map(Result::ok)
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.ends_with(".rs") && n != "mod.rs")
.collect();
actual.sort();
let missing: Vec<&String> = expected.iter().filter(|f| !actual.contains(f)).collect();
let extra: Vec<&String> = actual.iter().filter(|f| !expected.contains(f)).collect();
assert!(
missing.is_empty() && extra.is_empty(),
"generated code is out of sync with the schemas; run `just generate`\n\
missing: {missing:?}\nstale: {extra:?}"
);
}
#[test]
fn every_struct_stamps_the_schema_declared_version() {
let mut checked = 0usize;
for category in ["bo", "com", ""] {
for (title, doc) in read_schemas(&schema_dir().join(category)) {
let Some(declared) = metadata_literal(&doc, "_version") else {
continue;
};
assert!(
!declared.starts_with('v'),
"{title}: schema declares _version {declared:?} with a `v` prefix — \
re-check the assumption this guard rests on"
);
let src = generated_source_flat(&title);
let via_default = format!("version:Some({declared:?}.to_owned()),");
let via_builder =
format!("builder(default=Some({declared:?}.to_owned()),setter(into))");
assert!(
src.contains(&via_default) || src.contains(&via_builder),
"{title}: neither the generated Default nor the builder stamps \
`_version` as {declared:?}; run `just generate`"
);
checked += 1;
}
}
assert!(
checked >= 90,
"expected ~93 versioned schemas, saw {checked}"
);
}
#[test]
fn every_struct_stamps_the_schema_declared_typ() {
let mut checked = 0usize;
for (category, typ_enum) in [("bo", "BoTyp"), ("com", "ComTyp"), ("", "ComTyp")] {
for (title, doc) in read_schemas(&schema_dir().join(category)) {
let Some(declared) = metadata_literal(&doc, "_typ") else {
continue;
};
let src = generated_source_flat(&title);
let via_default = format!("typ:Some({typ_enum}::{title}),");
let via_builder = format!("builder(default=Some({typ_enum}::{title}),setter(skip))");
assert!(
src.contains(&via_default) || src.contains(&via_builder),
"{title}: neither the generated Default nor the builder stamps \
`_typ` for wire value {declared:?}; run `just generate`"
);
checked += 1;
}
}
assert!(
checked >= 90,
"expected ~95 schemas with a _typ constant, saw {checked}"
);
}
#[test]
fn bo_constants_agree_with_the_schema_and_each_other() {
let mut checked = 0usize;
for (title, doc) in read_schemas(&schema_dir().join("bo")) {
let Some(declared) = metadata_literal(&doc, "_typ") else {
continue;
};
let src = generated_source_flat(&title);
assert!(
src.contains(&format!("constTYP_WIRE:&'staticstr=\"{declared}\";")),
"{title}: TYP_WIRE does not carry the schema's `_typ` const {declared:?}; \
run `just generate`"
);
assert!(
src.contains(&format!("constBO_TYP:BoTyp=BoTyp::{title};")),
"{title}: BO_TYP is not the variant named after the struct; run `just generate`"
);
checked += 1;
}
assert!(checked >= 30, "expected ~35 BO schemas, saw {checked}");
}
#[test]
fn typ_wire_is_the_discriminants_own_wire_string() {
use rubo4e::current::{
Bilanzierung, Lastgang, Marktlokation, Messlokation, Netzlokation, Rechnung, Tarif, Vertrag,
};
use rubo4e::{Bo4eEnum as _, Bo4eObject};
fn check<T: Bo4eObject>(expected: &str) {
assert_eq!(T::TYP_WIRE, T::BO_TYP.as_wire(), "TYP_WIRE vs BO_TYP");
assert_eq!(T::TYP_WIRE, expected);
assert_eq!(
T::SCHEMA_SERIES,
T::SCHEMA_VERSION.split('.').next().expect("a version"),
"SCHEMA_SERIES must be SCHEMA_VERSION's YYYYMM prefix"
);
assert!(
!T::SCHEMA_VERSION.starts_with('v'),
"SCHEMA_VERSION is the wire spelling, which has no `v`"
);
}
check::<Marktlokation>("MARKTLOKATION");
check::<Messlokation>("MESSLOKATION");
check::<Netzlokation>("NETZLOKATION");
check::<Vertrag>("VERTRAG");
check::<Rechnung>("RECHNUNG");
check::<Bilanzierung>("BILANZIERUNG");
check::<Lastgang>("LASTGANG");
check::<Tarif>("TARIF");
}
#[test]
fn structs_with_required_fields_get_a_constructor() {
let mut checked = 0usize;
for category in ["bo", "com"] {
for (title, doc) in read_schemas(&schema_dir().join(category)) {
let required: Vec<&str> = doc
.get("required")
.and_then(|r| r.as_array())
.map(|a| a.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
let src = generated_source_flat(&title);
if required.is_empty() {
assert!(
src.contains(&format!("implDefaultfor{title}")),
"{title} has no required field, so it must derive or emit `Default`"
);
continue;
}
assert!(
!src.contains(&format!("implDefaultfor{title}")),
"{title} has required fields {required:?}, so it cannot have a `Default`"
);
assert!(
src.contains("pubfnnew("),
"{title} has required fields {required:?} and no `Default`, so it needs \
a `new(...)`; run `just generate`"
);
checked += 1;
}
}
assert_eq!(
checked, 2,
"v202607 declares `required` on exactly Lastgang and Tarif — if that changed, \
re-read the schemas before updating this number"
);
}
#[test]
fn discriminant_variants_are_named_after_their_structs() {
for (category, enum_file) in [("bo", "bo_typ.rs"), ("com", "com_typ.rs")] {
let src = std::fs::read_to_string(generated_dir().join(enum_file))
.unwrap_or_else(|e| panic!("cannot read {enum_file}: {e}"));
for (title, doc) in read_schemas(&schema_dir().join(category)) {
let Some(wire) = metadata_literal(&doc, "_typ") else {
continue;
};
let expected = format!(" {title},\n");
assert!(
src.contains(&expected),
"{enum_file}: wire value {wire:?} must map to the variant {title:?}, \
named after the struct it discriminates"
);
}
}
}
#[test]
fn enum_variants_are_injective_over_wire_values() {
for (title, doc) in read_schemas(&schema_dir().join("enum")) {
let Some(values) = doc["enum"].as_array() else {
continue;
};
let src = generated_source_flat(&title);
let mut by_variant: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for chunk in src.split("Self::").skip(1) {
let Some((variant, tail)) = chunk.split_once("=>") else {
continue;
};
let tail = tail.trim_start_matches('{');
let Some(rest) = tail.strip_prefix('"') else {
continue; };
let Some((wire, _)) = rest.split_once('"') else {
continue;
};
by_variant.entry(variant).or_default().push(wire);
}
for (variant, wires) in &by_variant {
assert_eq!(
wires.len(),
1,
"{title}::{variant} is claimed by {wires:?} — two distinct BO4E \
values collapsed onto one Rust variant"
);
}
let mapped = by_variant.len();
assert_eq!(
mapped,
values.len() + 1,
"{title}: {mapped} variants mapped but the schema declares {} values",
values.len(),
);
}
}