use std::collections::BTreeMap;
use crate::v1_1::bmm_persistence::error::PBmmReadError;
use crate::v1_1::bmm_persistence::p_bmm_class::PBmmClass;
use crate::v1_1::bmm_persistence::p_bmm_package::PBmmPackage;
use crate::v1_1::bmm_persistence::p_bmm_schema::PBmmSchema;
pub fn resolve_includes(
root: PBmmSchema,
loaded: &BTreeMap<String, PBmmSchema>,
) -> Result<PBmmSchema, PBmmReadError> {
let mut by_id: BTreeMap<String, &PBmmSchema> = BTreeMap::new();
for schema in loaded.values() {
let id = schema.schema_id();
if by_id.insert(id.clone(), schema).is_some() {
return Err(PBmmReadError::DuplicateSchemaId { id });
}
}
let mut chain: Vec<String> = Vec::new();
resolve(root, &by_id, &mut chain)
}
fn resolve(
schema: PBmmSchema,
by_id: &BTreeMap<String, &PBmmSchema>,
chain: &mut Vec<String>,
) -> Result<PBmmSchema, PBmmReadError> {
let id = schema.schema_id();
if chain.contains(&id) {
let mut cycle = chain.clone();
cycle.push(id);
return Err(PBmmReadError::IncludeCycle {
chain: cycle.join(" -> "),
});
}
chain.push(id.clone());
let mut merged = schema;
let include_ids: Vec<String> = merged
.includes
.iter()
.flat_map(BTreeMap::values)
.map(|spec| spec.id.clone())
.collect();
for include_id in include_ids {
let key = include_id.to_lowercase();
let Some(included) = by_id.get(&key) else {
return Err(PBmmReadError::MissingInclude {
requester: id.clone(),
id: include_id,
});
};
let included = resolve((*included).clone(), by_id, chain)?;
absorb(&mut merged, included);
}
chain.pop();
Ok(merged)
}
fn absorb(target: &mut PBmmSchema, source: PBmmSchema) {
absorb_classes(
target,
source.primitive_types.unwrap_or_default(),
ClassList::Primitive,
);
absorb_classes(
target,
source.class_definitions.unwrap_or_default(),
ClassList::Definitions,
);
merge_packages(&mut target.packages, source.packages);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ClassList {
Primitive,
Definitions,
}
fn absorb_classes(
includer: &mut PBmmSchema,
incoming: BTreeMap<String, PBmmClass>,
list: ClassList,
) {
for (key, class) in incoming {
if let Some(existing) = find_class_mut(includer, class.name()) {
existing.set_is_override(true);
continue;
}
let target = match list {
ClassList::Primitive => &mut includer.primitive_types,
ClassList::Definitions => &mut includer.class_definitions,
};
target.get_or_insert_default().insert(key, class);
}
}
fn find_class_mut<'a>(schema: &'a mut PBmmSchema, name: &str) -> Option<&'a mut PBmmClass> {
schema
.primitive_types
.iter_mut()
.flat_map(BTreeMap::values_mut)
.chain(
schema
.class_definitions
.iter_mut()
.flat_map(BTreeMap::values_mut),
)
.find(|class| class.name() == name)
}
fn merge_packages(
target: &mut BTreeMap<String, PBmmPackage>,
source: BTreeMap<String, PBmmPackage>,
) {
for (key, package) in source {
match target.get_mut(&key) {
None => {
target.insert(key, package);
}
Some(existing) => {
for class in package.classes.into_iter().flatten() {
let existing_classes = existing.classes.get_or_insert_default();
if !existing_classes.contains(&class) {
existing_classes.push(class);
}
}
merge_packages(&mut existing.packages, package.packages);
}
}
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::panic_in_result_fn,
reason = "the Book ch11 test shape: `?` propagates the read/resolve/model plumbing while the assertions ARE the test — an assertion panic is how these tests fail"
)]
use std::collections::BTreeMap;
use std::fmt::Write;
use crate::v1_1::bmm_persistence::error::PBmmReadError;
use crate::v1_1::bmm_persistence::include_resolution::resolve_includes;
use crate::v1_1::bmm_persistence::p_bmm_class::PBmmClass;
use crate::v1_1::bmm_persistence::p_bmm_schema::PBmmSchema;
use crate::v1_1::bmm_persistence::reader::read_schema;
fn read(src: &str) -> PBmmSchema {
read_schema(src).expect("the fixture reads")
}
fn loaded(schemas: Vec<PBmmSchema>) -> BTreeMap<String, PBmmSchema> {
schemas
.into_iter()
.map(|schema| (schema.schema_id(), schema))
.collect()
}
fn schema_src(name: &str, includes: &[&str], classes: &[(&str, &str)]) -> String {
let mut src = format!(
r#"
bmm_version = <"2.4">
rm_publisher = <"openehr">
schema_name = <"{name}">
rm_release = <"1.0.2">
"#
);
if !includes.is_empty() {
src.push_str("includes = <\n");
for (index, id) in includes.iter().enumerate() {
let key = index + 1;
let _ = writeln!(src, "[\"{key}\"] = < id = <\"{id}\"> >");
}
src.push_str(">\n");
}
let names: Vec<String> = classes
.iter()
.map(|(class, _)| format!("\"{class}\""))
.collect();
let _ = writeln!(
src,
"packages = <\n[\"org.openehr.{name}\"] = <\nname = <\"org.openehr.{name}\">\nclasses = <{}>\n>\n>",
names.join(", ")
);
src.push_str("class_definitions = <\n");
for (class, documentation) in classes {
let _ = writeln!(
src,
"[\"{class}\"] = < name = <\"{class}\"> documentation = <\"{documentation}\"> >"
);
}
src.push_str(">\n");
src
}
#[test]
fn inclusion_is_transitive() -> Result<(), PBmmReadError> {
let leaf = read(&schema_src("primitive_types", &[], &[("Any", "leaf")]));
let middle = read(&schema_src(
"basic_types",
&["openehr_primitive_types_1.0.2"],
&[("DV_TEXT", "middle")],
));
let root = read(&schema_src(
"structures",
&["openehr_basic_types_1.0.2"],
&[("ELEMENT", "root")],
));
let merged = resolve_includes(root, &loaded(vec![leaf, middle]))?;
let mut names: Vec<&str> = merged
.class_definitions
.iter()
.flat_map(BTreeMap::values)
.map(PBmmClass::name)
.collect();
names.sort_unstable();
assert_eq!(names, ["Any", "DV_TEXT", "ELEMENT"]);
assert_eq!(merged.schema_id(), "openehr_structures_1.0.2");
assert_eq!(merged.includes.as_ref().map(BTreeMap::len), Some(1));
Ok(())
}
#[test]
fn the_includer_wins_a_name_collision_and_is_marked_as_an_override() -> Result<(), PBmmReadError>
{
let included = read(&schema_src(
"primitive_types",
&[],
&[("Any", "from the included schema")],
));
let root = read(&schema_src(
"structures",
&["openehr_primitive_types_1.0.2"],
&[("Any", "from the including schema")],
));
let merged = resolve_includes(root, &loaded(vec![included]))?;
assert_eq!(
merged.class_definitions.as_ref().map_or(0, BTreeMap::len),
1
);
let class = merged
.class_definitions
.iter()
.flat_map(BTreeMap::values)
.next()
.expect("the surviving definition");
assert_eq!(class.documentation(), Some("from the including schema"));
assert!(class.is_override());
Ok(())
}
#[test]
fn every_class_keeps_the_schema_id_that_defined_it() -> Result<(), PBmmReadError> {
let included = read(&schema_src("primitive_types", &[], &[("Any", "leaf")]));
let root = read(&schema_src(
"structures",
&["openehr_primitive_types_1.0.2"],
&[("ELEMENT", "root")],
));
let merged = resolve_includes(root, &loaded(vec![included]))?;
let sources: BTreeMap<&str, Option<&str>> = merged
.class_definitions
.iter()
.flat_map(BTreeMap::values)
.map(|class| (class.name(), class.source_schema_id()))
.collect();
assert_eq!(
sources.get("Any").copied().flatten(),
Some("openehr_primitive_types_1.0.2")
);
assert_eq!(
sources.get("ELEMENT").copied().flatten(),
Some("openehr_structures_1.0.2")
);
Ok(())
}
#[test]
fn an_included_interface_joins_the_class_list_and_loses_to_the_includer()
-> Result<(), PBmmReadError> {
let included = read(
r#"
bmm_version = <"2.4">
rm_publisher = <"openehr">
schema_name = <"leaf">
rm_release = <"1.0.2">
packages = <
["org.openehr.leaf"] = <
name = <"org.openehr.leaf">
classes = <"Math", "TERMINOLOGY_ACCESS">
>
>
class_definitions = <
["Math"] = (P_BMM_INTERFACE) < name = <"Math"> >
["TERMINOLOGY_ACCESS"] = (P_BMM_INTERFACE) <
name = <"TERMINOLOGY_ACCESS">
documentation = <"from the included schema">
>
>
"#,
);
let root = read(
r#"
bmm_version = <"2.4">
rm_publisher = <"openehr">
schema_name = <"root">
rm_release = <"1.0.2">
includes = <
["1"] = < id = <"openehr_leaf_1.0.2"> >
>
packages = <
["org.openehr.root"] = <
name = <"org.openehr.root">
classes = <"TERMINOLOGY_ACCESS">
>
>
class_definitions = <
["TERMINOLOGY_ACCESS"] = (P_BMM_INTERFACE) <
name = <"TERMINOLOGY_ACCESS">
documentation = <"from the including schema">
>
>
"#,
);
let merged = resolve_includes(root, &loaded(vec![included]))?;
let mut names: Vec<&str> = merged
.class_definitions
.iter()
.flat_map(BTreeMap::values)
.map(PBmmClass::name)
.collect();
names.sort_unstable();
assert_eq!(names, ["Math", "TERMINOLOGY_ACCESS"]);
let surviving = merged
.class_definitions
.iter()
.flat_map(BTreeMap::values)
.find(|class| class.name() == "TERMINOLOGY_ACCESS")
.expect("the surviving definition");
assert_eq!(surviving.documentation(), Some("from the including schema"));
assert!(!surviving.is_override());
Ok(())
}
#[test]
fn packages_merge_recursively() -> Result<(), PBmmReadError> {
let included = read(
r#"
bmm_version = <"2.4">
rm_publisher = <"openehr">
schema_name = <"leaf">
rm_release = <"1.0.2">
packages = <
["org.openehr.rm"] = <
name = <"org.openehr.rm">
classes = <"Any">
packages = <
["support"] = <
name = <"support">
classes = <"TERMINOLOGY_ID">
>
>
>
>
class_definitions = <
["Any"] = < name = <"Any"> >
["TERMINOLOGY_ID"] = < name = <"TERMINOLOGY_ID"> >
>
"#,
);
let root = read(
r#"
bmm_version = <"2.4">
rm_publisher = <"openehr">
schema_name = <"root">
rm_release = <"1.0.2">
includes = <
["1"] = < id = <"openehr_leaf_1.0.2"> >
>
packages = <
["org.openehr.rm"] = <
name = <"org.openehr.rm">
classes = <"ELEMENT">
packages = <
["composition"] = <
name = <"composition">
classes = <"COMPOSITION">
>
>
>
>
class_definitions = <
["ELEMENT"] = < name = <"ELEMENT"> >
["COMPOSITION"] = < name = <"COMPOSITION"> >
>
"#,
);
let merged = resolve_includes(root, &loaded(vec![included]))?;
let rm = merged
.packages
.get("org.openehr.rm")
.expect("the shared top-level package");
assert_eq!(
rm.classes,
Some(["ELEMENT".to_owned(), "Any".to_owned()].to_vec())
);
assert_eq!(rm.packages.len(), 2);
assert!(rm.packages.contains_key("composition"));
assert!(rm.packages.contains_key("support"));
Ok(())
}
#[test]
fn a_missing_include_is_refused() {
let root = read(&schema_src("structures", &["openehr_absent_9.9.9"], &[]));
let error = resolve_includes(root, &BTreeMap::new()).expect_err("the include is absent");
assert_eq!(
error,
PBmmReadError::MissingInclude {
requester: "openehr_structures_1.0.2".to_owned(),
id: "openehr_absent_9.9.9".to_owned(),
}
);
}
#[test]
fn an_inclusion_cycle_is_refused() {
let first = read(&schema_src("first", &["openehr_second_1.0.2"], &[]));
let second = read(&schema_src("second", &["openehr_first_1.0.2"], &[]));
let error = resolve_includes(first.clone(), &loaded(vec![first, second]))
.expect_err("the graph is cyclic");
assert!(
matches!(error, PBmmReadError::IncludeCycle { .. }),
"expected a cycle refusal, got {error:?}"
);
}
}