#![expect(
clippy::disallowed_types,
reason = "ODIN-to-JSON conversion targets the JSON data model by specification (LANG odin \
spec) (#1694)"
)]
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use indexmap::IndexMap;
use openehr_base::v1_3::prelude::Interval;
use openehr_base::v1_3::prelude::ProperInterval;
use openehr_base::v1_3::prelude::ProperIntervalData;
use crate::v1_1::bmm::core::bmm_include_spec::BmmIncludeSpec;
use crate::v1_1::bmm_persistence::error::PBmmReadError;
use crate::v1_1::bmm_persistence::p_bmm_base_type::PBmmBaseType;
use crate::v1_1::bmm_persistence::p_bmm_class::PBmmClass;
use crate::v1_1::bmm_persistence::p_bmm_class::PBmmClassData;
use crate::v1_1::bmm_persistence::p_bmm_constant::PBmmConstant;
use crate::v1_1::bmm_persistence::p_bmm_container_function_parameter::PBmmContainerFunctionParameter;
use crate::v1_1::bmm_persistence::p_bmm_container_property::PBmmContainerProperty;
use crate::v1_1::bmm_persistence::p_bmm_container_property::PBmmContainerPropertyData;
use crate::v1_1::bmm_persistence::p_bmm_container_type::PBmmContainerType;
use crate::v1_1::bmm_persistence::p_bmm_container_type::PBmmContainerTypeData;
use crate::v1_1::bmm_persistence::p_bmm_enumeration::PBmmEnumeration;
use crate::v1_1::bmm_persistence::p_bmm_enumeration::PBmmEnumerationData;
use crate::v1_1::bmm_persistence::p_bmm_enumeration_integer::PBmmEnumerationInteger;
use crate::v1_1::bmm_persistence::p_bmm_enumeration_string::PBmmEnumerationString;
use crate::v1_1::bmm_persistence::p_bmm_function::PBmmFunction;
use crate::v1_1::bmm_persistence::p_bmm_function_parameter::PBmmFunctionParameter;
use crate::v1_1::bmm_persistence::p_bmm_generic_function_parameter::PBmmGenericFunctionParameter;
use crate::v1_1::bmm_persistence::p_bmm_generic_parameter::PBmmGenericParameter;
use crate::v1_1::bmm_persistence::p_bmm_generic_property::PBmmGenericProperty;
use crate::v1_1::bmm_persistence::p_bmm_generic_type::PBmmGenericType;
use crate::v1_1::bmm_persistence::p_bmm_indexed_container_property::PBmmIndexedContainerProperty;
use crate::v1_1::bmm_persistence::p_bmm_indexed_container_type::PBmmIndexedContainerType;
use crate::v1_1::bmm_persistence::p_bmm_interface::PBmmInterface;
use crate::v1_1::bmm_persistence::p_bmm_open_type::PBmmOpenType;
use crate::v1_1::bmm_persistence::p_bmm_package::PBmmPackage;
use crate::v1_1::bmm_persistence::p_bmm_property::PBmmProperty;
use crate::v1_1::bmm_persistence::p_bmm_schema::PBmmSchema;
use crate::v1_1::bmm_persistence::p_bmm_schema_impl::compose_schema_id;
use crate::v1_1::bmm_persistence::p_bmm_simple_type::PBmmSimpleType;
use crate::v1_1::bmm_persistence::p_bmm_single_function_parameter::PBmmSingleFunctionParameter;
use crate::v1_1::bmm_persistence::p_bmm_single_function_parameter_open::PBmmSingleFunctionParameterOpen;
use crate::v1_1::bmm_persistence::p_bmm_single_property::PBmmSingleProperty;
use crate::v1_1::bmm_persistence::p_bmm_single_property_open::PBmmSinglePropertyOpen;
use crate::v1_1::bmm_persistence::p_bmm_type::PBmmType;
use crate::v1_1::odin::OdinInterval;
use crate::v1_1::odin::OdinKey;
use crate::v1_1::odin::OdinValue;
use openehr_base::containers::present;
pub const TOLERATED_SCHEMA_ATTRIBUTES: &[&str] = &["model_name"];
pub const TOLERATED_PROPERTY_ATTRIBUTES: &[&str] = &["default"];
pub fn read_schema(src: &str) -> Result<PBmmSchema, PBmmReadError> {
let root = crate::v1_1::odin::parse(src)?;
let OdinValue::Object(members) = &root else {
return Err(PBmmReadError::NotASchemaObject {
found: kind_name(&root),
});
};
let mut block = Block::new(String::new(), members);
let bmm_version = block.required_string("bmm_version")?;
let rm_publisher = block.required_string("rm_publisher")?;
let schema_name = block.required_string("schema_name")?;
let rm_release = block.required_string("rm_release")?;
let schema_id = compose_schema_id(&rm_publisher, &schema_name, &rm_release);
let schema_revision = block.documentation_string("schema_revision")?;
let schema_lifecycle_state = block.documentation_string("schema_lifecycle_state")?;
let schema_author = block.documentation_string("schema_author")?;
let schema_description = block.documentation_string("schema_description")?;
let schema_contributors = block.string_list("schema_contributors")?;
let archetype_parent_class = block.optional_string("archetype_parent_class")?;
let archetype_data_value_parent_class =
block.optional_string("archetype_data_value_parent_class")?;
let archetype_rm_closure_packages = block.string_list("archetype_rm_closure_packages")?;
let archetype_visualise_descendants_of = block.visualise_descendants_of()?;
let includes = read_includes(&mut block)?;
let mut uid: i32 = 0;
let primitive_types = read_class_list(&mut block, "primitive_types", &schema_id, &mut uid)?;
let class_definitions = read_class_list(&mut block, "class_definitions", &schema_id, &mut uid)?;
let packages = read_packages(&mut block, "packages", PackageLevel::Top)?;
block.finish(TOLERATED_SCHEMA_ATTRIBUTES)?;
Ok(PBmmSchema {
packages,
rm_publisher,
rm_release,
schema_name,
schema_revision,
schema_lifecycle_state,
schema_author,
schema_description,
schema_contributors: present(schema_contributors),
archetype_parent_class,
archetype_data_value_parent_class,
archetype_rm_closure_packages: present(archetype_rm_closure_packages),
archetype_visualise_descendants_of,
bmm_version,
includes,
primitive_types: present(primitive_types),
class_definitions: present(class_definitions),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PackageLevel {
Top,
Nested,
}
fn kind_name(value: &OdinValue) -> &'static str {
match value {
OdinValue::Object(_) => "an attribute object",
OdinValue::KeyedList(_) => "a keyed list",
OdinValue::List(_) => "a list",
OdinValue::Typed { .. } => "a type-marked value",
OdinValue::PathList(_) | OdinValue::Path(_) => "an object reference",
OdinValue::Empty => "an empty block",
OdinValue::String(_) => "a string",
OdinValue::Integer(_) => "an integer",
OdinValue::Real(_) => "a real",
OdinValue::Boolean(_) => "a boolean",
OdinValue::Character(_) => "a character",
OdinValue::Date(_) => "a date",
OdinValue::Time(_) => "a time",
OdinValue::DateTime(_) => "a date/time",
OdinValue::Duration(_) => "a duration",
OdinValue::Interval(_) => "an interval",
OdinValue::TermCode(_) => "a term code",
OdinValue::Uri(_) => "a URI",
OdinValue::PlugIn { .. } => "a plug-in-syntax block",
OdinValue::ListContinue => "a list-continuation marker",
}
}
fn key_text(key: &OdinKey) -> String {
match key {
OdinKey::String(text)
| OdinKey::Date(text)
| OdinKey::Time(text)
| OdinKey::DateTime(text) => text.clone(),
OdinKey::Integer(value) => value.to_string(),
}
}
fn join_path(parent: &str, segment: &str) -> String {
if parent.is_empty() {
segment.to_owned()
} else {
format!("{parent}/{segment}")
}
}
struct Block<'a> {
path: String,
members: &'a IndexMap<String, OdinValue>,
consumed: BTreeSet<&'a str>,
}
impl<'a> Block<'a> {
fn new(path: String, members: &'a IndexMap<String, OdinValue>) -> Self {
Self {
path,
members,
consumed: BTreeSet::new(),
}
}
fn open(
path: &str,
value: &'a OdinValue,
empty: &'a IndexMap<String, OdinValue>,
) -> Result<Self, PBmmReadError> {
match value {
OdinValue::Object(members) => Ok(Self::new(path.to_owned(), members)),
OdinValue::Empty => Ok(Self::new(path.to_owned(), empty)),
other => Err(PBmmReadError::WrongValueShape {
path: path.to_owned(),
expected: "an attribute object",
found: kind_name(other),
}),
}
}
fn take(&mut self, name: &'static str) -> Option<&'a OdinValue> {
self.consumed.insert(name);
self.members.get(name)
}
fn member_path(&self, name: &str) -> String {
join_path(&self.path, name)
}
fn required_string(&mut self, name: &'static str) -> Result<String, PBmmReadError> {
let path = self.member_path(name);
match self.take(name) {
Some(value) => as_string(&path, value),
None => Err(PBmmReadError::MissingAttribute {
path: self.path.clone(),
attribute: name,
}),
}
}
fn optional_string(&mut self, name: &'static str) -> Result<Option<String>, PBmmReadError> {
let path = self.member_path(name);
match self.take(name) {
Some(value) => as_string(&path, value).map(Some),
None => Ok(None),
}
}
fn documentation_string(&mut self, name: &'static str) -> Result<String, PBmmReadError> {
Ok(self.optional_string(name)?.unwrap_or_default())
}
fn visualise_descendants_of(&mut self) -> Result<Option<String>, PBmmReadError> {
let spec_spelling = self.optional_string("archetype_visualise_descendants_of")?;
let variant_spelling = self.optional_string("archetype_visualize_descendants_of")?;
Ok(spec_spelling.or(variant_spelling))
}
fn literal_text(&mut self, name: &'static str) -> Result<Option<String>, PBmmReadError> {
let path = self.member_path(name);
match self.take(name) {
None => Ok(None),
Some(value) => literal_text_of(&path, value).map(Some),
}
}
fn optional_bool(&mut self, name: &'static str) -> Result<Option<bool>, PBmmReadError> {
let path = self.member_path(name);
match self.take(name) {
Some(OdinValue::Boolean(flag)) => Ok(Some(*flag)),
Some(other) => Err(PBmmReadError::WrongValueShape {
path,
expected: "a boolean",
found: kind_name(other),
}),
None => Ok(None),
}
}
fn string_list(&mut self, name: &'static str) -> Result<Vec<String>, PBmmReadError> {
let path = self.member_path(name);
match self.take(name) {
Some(value) => string_list_of(&path, value),
None => Ok(Vec::new()),
}
}
fn string_map(
&mut self,
name: &'static str,
) -> Result<Option<BTreeMap<String, String>>, PBmmReadError> {
let path = self.member_path(name);
let Some(value) = self.take(name) else {
return Ok(None);
};
let mut out = BTreeMap::new();
for (key, item) in keyed_entries(&path, value)? {
let key = key_text(key);
let entry_path = join_path(&path, &key);
let text = as_string(&entry_path, item)?;
out.insert(key, text);
}
Ok(Some(out))
}
fn cardinality(&mut self) -> Result<Option<Interval<i32>>, PBmmReadError> {
let path = self.member_path("cardinality");
match self.take("cardinality") {
Some(OdinValue::Interval(interval)) => integer_interval(&path, interval).map(Some),
Some(other) => Err(PBmmReadError::WrongValueShape {
path,
expected: "an interval",
found: kind_name(other),
}),
None => Ok(None),
}
}
fn finish(&self, tolerated: &[&str]) -> Result<(), PBmmReadError> {
for name in self.members.keys() {
if self.consumed.contains(name.as_str()) || tolerated.contains(&name.as_str()) {
continue;
}
return Err(PBmmReadError::UnknownAttribute {
path: self.path.clone(),
attribute: name.clone(),
});
}
Ok(())
}
}
fn as_string(path: &str, value: &OdinValue) -> Result<String, PBmmReadError> {
match value {
OdinValue::String(text) => Ok(text.clone()),
other => Err(PBmmReadError::WrongValueShape {
path: path.to_owned(),
expected: "a string",
found: kind_name(other),
}),
}
}
fn literal_text_of(path: &str, value: &OdinValue) -> Result<String, PBmmReadError> {
match value {
OdinValue::String(text)
| OdinValue::Date(text)
| OdinValue::Time(text)
| OdinValue::DateTime(text)
| OdinValue::Duration(text)
| OdinValue::TermCode(text)
| OdinValue::Uri(text) => Ok(text.clone()),
OdinValue::Integer(number) => Ok(number.to_string()),
OdinValue::Real(number) => Ok(number.to_string()),
OdinValue::Character(character) => Ok(character.to_string()),
OdinValue::Boolean(true) => Ok("True".to_owned()),
OdinValue::Boolean(false) => Ok("False".to_owned()),
other => Err(PBmmReadError::WrongValueShape {
path: path.to_owned(),
expected: "a scalar literal",
found: kind_name(other),
}),
}
}
fn string_list_of(path: &str, value: &OdinValue) -> Result<Vec<String>, PBmmReadError> {
match value {
OdinValue::String(text) => Ok(vec![text.clone()]),
OdinValue::Empty => Ok(Vec::new()),
OdinValue::List(items) => items
.iter()
.filter(|item| !matches!(item, OdinValue::ListContinue))
.map(|item| as_string(path, item))
.collect(),
other => Err(PBmmReadError::WrongValueShape {
path: path.to_owned(),
expected: "a string list",
found: kind_name(other),
}),
}
}
fn any_list_of(path: &str, value: &OdinValue) -> Result<Vec<serde_json::Value>, PBmmReadError> {
match value {
OdinValue::Empty => Ok(Vec::new()),
OdinValue::List(items) => items
.iter()
.filter(|item| !matches!(item, OdinValue::ListContinue))
.map(|item| any_scalar(path, item))
.collect(),
single => any_scalar(path, single).map(|scalar| vec![scalar]),
}
}
fn any_scalar(path: &str, value: &OdinValue) -> Result<serde_json::Value, PBmmReadError> {
match value {
OdinValue::String(text) => Ok(serde_json::Value::String(text.clone())),
OdinValue::Integer(number) => Ok(serde_json::Value::from(*number)),
OdinValue::Boolean(flag) => Ok(serde_json::Value::Bool(*flag)),
OdinValue::Character(character) => Ok(serde_json::Value::String(character.to_string())),
OdinValue::Real(number) => serde_json::Number::from_f64(*number)
.map(serde_json::Value::Number)
.ok_or_else(|| PBmmReadError::WrongValueShape {
path: path.to_owned(),
expected: "a finite real",
found: "a non-finite real",
}),
other => Err(PBmmReadError::WrongValueShape {
path: path.to_owned(),
expected: "an enumeration item value",
found: kind_name(other),
}),
}
}
fn keyed_entries<'a>(
path: &str,
value: &'a OdinValue,
) -> Result<&'a [(OdinKey, OdinValue)], PBmmReadError> {
match value {
OdinValue::KeyedList(entries) => Ok(entries.as_slice()),
OdinValue::Empty => Ok(&[]),
other => Err(PBmmReadError::WrongValueShape {
path: path.to_owned(),
expected: "a keyed list",
found: kind_name(other),
}),
}
}
fn integer_interval(path: &str, interval: &OdinInterval) -> Result<Interval<i32>, PBmmReadError> {
let OdinInterval::Range {
lower,
lower_included,
upper,
upper_included,
} = interval
else {
return Err(PBmmReadError::UnsupportedCardinality {
path: path.to_owned(),
});
};
let lower = interval_bound(path, lower.as_deref())?;
let upper = interval_bound(path, upper.as_deref())?;
Ok(Interval::ProperInterval(ProperInterval::ProperInterval(
ProperIntervalData {
lower,
upper,
lower_unbounded: lower.is_none(),
upper_unbounded: upper.is_none(),
lower_included: *lower_included,
upper_included: *upper_included,
},
)))
}
fn interval_bound(path: &str, bound: Option<&OdinValue>) -> Result<Option<i32>, PBmmReadError> {
match bound {
None => Ok(None),
Some(OdinValue::Integer(number)) => i32::try_from(*number).map(Some).map_err(|_overflow| {
PBmmReadError::UnsupportedCardinality {
path: path.to_owned(),
}
}),
Some(_) => Err(PBmmReadError::UnsupportedCardinality {
path: path.to_owned(),
}),
}
}
fn split_marker(value: &OdinValue) -> (Option<&str>, &OdinValue) {
match value {
OdinValue::Typed { rm_type, value } => (Some(rm_type.as_str()), value.as_ref()),
plain => (None, plain),
}
}
fn read_includes(
block: &mut Block<'_>,
) -> Result<Option<BTreeMap<String, BmmIncludeSpec>>, PBmmReadError> {
let path = block.member_path("includes");
let Some(value) = block.take("includes") else {
return Ok(None);
};
let empty = IndexMap::new();
let mut out = BTreeMap::new();
for (key, entry) in keyed_entries(&path, value)? {
let entry_path = join_path(&path, &key_text(key));
let mut entry_block = Block::open(&entry_path, entry, &empty)?;
let id = entry_block.required_string("id")?;
entry_block.finish(&[])?;
out.insert(id.clone(), BmmIncludeSpec { id });
}
Ok(Some(out))
}
fn read_packages(
block: &mut Block<'_>,
name: &'static str,
level: PackageLevel,
) -> Result<BTreeMap<String, PBmmPackage>, PBmmReadError> {
let path = block.member_path(name);
let Some(value) = block.take(name) else {
return Ok(BTreeMap::new());
};
let empty = IndexMap::new();
let mut out = BTreeMap::new();
for (key, entry) in keyed_entries(&path, value)? {
let key = key_text(key);
let entry_path = join_path(&path, &key);
let mut entry_block = Block::open(&entry_path, entry, &empty)?;
let package_name = read_name(&mut entry_block, &key)?;
if level == PackageLevel::Nested && package_name.contains('.') {
return Err(PBmmReadError::QualifiedNestedPackage {
path: entry_path,
name: package_name,
});
}
let documentation = entry_block.optional_string("documentation")?;
let classes = entry_block.string_list("classes")?;
let packages = read_packages(&mut entry_block, "packages", PackageLevel::Nested)?;
entry_block.finish(&[])?;
out.insert(
key,
PBmmPackage {
packages,
documentation,
name: package_name,
classes: present(classes),
bmm_package_definition: None,
},
);
}
Ok(out)
}
fn read_name(block: &mut Block<'_>, key: &str) -> Result<String, PBmmReadError> {
match block.optional_string("name")? {
None => Ok(key.to_owned()),
Some(name) if name == key => Ok(name),
Some(name) => Err(PBmmReadError::KeyNameMismatch {
path: block.path.clone(),
key: key.to_owned(),
name,
}),
}
}
fn read_class_list(
block: &mut Block<'_>,
name: &'static str,
schema_id: &str,
uid: &mut i32,
) -> Result<Vec<PBmmClass>, PBmmReadError> {
let path = block.member_path(name);
let Some(value) = block.take(name) else {
return Ok(Vec::new());
};
let mut out = Vec::new();
for (key, entry) in keyed_entries(&path, value)? {
let key = key_text(key);
let entry_path = join_path(&path, &key);
*uid = uid.saturating_add(1);
out.push(read_class(&entry_path, &key, entry, schema_id, *uid)?);
}
Ok(out)
}
struct ClassParts {
documentation: Option<String>,
name: String,
ancestors: Option<Vec<String>>,
constants: Option<BTreeMap<String, PBmmConstant>>,
properties: Option<BTreeMap<String, PBmmProperty>>,
functions: Option<BTreeMap<String, PBmmFunction>>,
invariants: Option<BTreeMap<String, String>>,
is_abstract: Option<bool>,
is_override: Option<bool>,
generic_parameter_defs: Option<BTreeMap<String, PBmmGenericParameter>>,
source_schema_id: String,
uid: i32,
ancestor_defs: Option<Vec<PBmmGenericType>>,
}
struct EnumerationParts {
item_names: Option<Vec<String>>,
item_values: Option<Vec<serde_json::Value>>,
item_documentations: Option<Vec<String>>,
}
fn read_class(
path: &str,
key: &str,
value: &OdinValue,
schema_id: &str,
uid: i32,
) -> Result<PBmmClass, PBmmReadError> {
let (marker, body) = split_marker(value);
let empty = IndexMap::new();
let mut block = Block::open(path, body, &empty)?;
if marker == Some("P_BMM_INTERFACE") {
let name = read_name(&mut block, key)?;
let documentation = block.optional_string("documentation")?;
let functions = read_functions(&mut block)?;
block.finish(&[])?;
return Ok(PBmmClass::PBmmInterface(PBmmInterface {
documentation,
name,
functions,
}));
}
let parts = read_class_parts(&mut block, key, schema_id, uid)?;
let class = match marker {
None | Some("P_BMM_CLASS") => {
block.finish(&[])?;
PBmmClass::PBmmClass(class_data(parts))
}
Some("P_BMM_ENUMERATION") => {
let items = read_enumeration_parts(&mut block)?;
block.finish(&[])?;
PBmmClass::PBmmEnumeration(PBmmEnumeration::PBmmEnumeration(enumeration_data(
parts, items,
)))
}
Some("P_BMM_ENUMERATION_INTEGER") => {
let items = read_enumeration_parts(&mut block)?;
block.finish(&[])?;
PBmmClass::PBmmEnumeration(PBmmEnumeration::PBmmEnumerationInteger(
enumeration_integer(parts, items),
))
}
Some("P_BMM_ENUMERATION_STRING") => {
let items = read_enumeration_parts(&mut block)?;
block.finish(&[])?;
PBmmClass::PBmmEnumeration(PBmmEnumeration::PBmmEnumerationString(enumeration_string(
parts, items,
)))
}
Some(other) => {
return Err(PBmmReadError::UnexpectedTypeMarker {
path: path.to_owned(),
marker: other.to_owned(),
expected: "P_BMM_CLASS",
});
}
};
Ok(class)
}
fn read_class_parts(
block: &mut Block<'_>,
key: &str,
schema_id: &str,
uid: i32,
) -> Result<ClassParts, PBmmReadError> {
let name = read_name(block, key)?;
Ok(ClassParts {
documentation: block.optional_string("documentation")?,
ancestors: present(block.string_list("ancestors")?),
constants: read_constants(block)?,
properties: read_properties(block)?,
functions: read_functions(block)?,
invariants: block.string_map("invariants")?,
is_abstract: block.optional_bool("is_abstract")?,
is_override: block.optional_bool("is_override")?,
generic_parameter_defs: read_generic_parameter_defs(block)?,
ancestor_defs: present(read_ancestor_defs(block)?),
source_schema_id: schema_id.to_owned(),
uid,
name,
})
}
fn read_enumeration_parts(block: &mut Block<'_>) -> Result<EnumerationParts, PBmmReadError> {
let values_path = block.member_path("item_values");
let item_values = match block.take("item_values") {
Some(value) => any_list_of(&values_path, value)?,
None => Vec::new(),
};
Ok(EnumerationParts {
item_names: present(block.string_list("item_names")?),
item_documentations: present(block.string_list("item_documentations")?),
item_values: present(item_values),
})
}
fn class_data(parts: ClassParts) -> PBmmClassData {
PBmmClassData {
documentation: parts.documentation,
name: parts.name,
ancestors: parts.ancestors,
constants: parts.constants,
properties: parts.properties,
functions: parts.functions,
invariants: parts.invariants,
is_abstract: parts.is_abstract,
is_override: parts.is_override,
generic_parameter_defs: parts.generic_parameter_defs,
source_schema_id: parts.source_schema_id,
bmm_class: None,
uid: parts.uid,
ancestor_defs: parts.ancestor_defs,
}
}
fn enumeration_data(parts: ClassParts, items: EnumerationParts) -> PBmmEnumerationData {
PBmmEnumerationData {
documentation: parts.documentation,
name: parts.name,
ancestors: parts.ancestors,
constants: parts.constants,
properties: parts.properties,
functions: parts.functions,
invariants: parts.invariants,
is_abstract: parts.is_abstract,
is_override: parts.is_override,
generic_parameter_defs: parts.generic_parameter_defs,
source_schema_id: parts.source_schema_id,
bmm_class: None,
uid: parts.uid,
ancestor_defs: parts.ancestor_defs,
item_names: items.item_names,
item_values: items.item_values,
item_documentations: items.item_documentations,
}
}
fn enumeration_integer(parts: ClassParts, items: EnumerationParts) -> PBmmEnumerationInteger {
PBmmEnumerationInteger {
documentation: parts.documentation,
name: parts.name,
ancestors: parts.ancestors,
constants: parts.constants,
properties: parts.properties,
functions: parts.functions,
invariants: parts.invariants,
is_abstract: parts.is_abstract,
is_override: parts.is_override,
generic_parameter_defs: parts.generic_parameter_defs,
source_schema_id: parts.source_schema_id,
bmm_class: None,
uid: parts.uid,
ancestor_defs: parts.ancestor_defs,
item_names: items.item_names,
item_values: items.item_values,
item_documentations: items.item_documentations,
}
}
fn enumeration_string(parts: ClassParts, items: EnumerationParts) -> PBmmEnumerationString {
PBmmEnumerationString {
documentation: parts.documentation,
name: parts.name,
ancestors: parts.ancestors,
constants: parts.constants,
properties: parts.properties,
functions: parts.functions,
invariants: parts.invariants,
is_abstract: parts.is_abstract,
is_override: parts.is_override,
generic_parameter_defs: parts.generic_parameter_defs,
source_schema_id: parts.source_schema_id,
bmm_class: None,
uid: parts.uid,
ancestor_defs: parts.ancestor_defs,
item_names: items.item_names,
item_values: items.item_values,
item_documentations: items.item_documentations,
}
}
fn read_generic_parameter_defs(
block: &mut Block<'_>,
) -> Result<Option<BTreeMap<String, PBmmGenericParameter>>, PBmmReadError> {
let path = block.member_path("generic_parameter_defs");
let Some(value) = block.take("generic_parameter_defs") else {
return Ok(None);
};
let empty = IndexMap::new();
let mut out = BTreeMap::new();
for (key, entry) in keyed_entries(&path, value)? {
let key = key_text(key);
let entry_path = join_path(&path, &key);
let mut entry_block = Block::open(&entry_path, entry, &empty)?;
let name = read_name(&mut entry_block, &key)?;
let documentation = entry_block.optional_string("documentation")?;
let conforms_to_type = entry_block.optional_string("conforms_to_type")?;
entry_block.finish(&[])?;
out.insert(
key,
PBmmGenericParameter {
documentation,
name,
conforms_to_type,
bmm_generic_parameter: None,
},
);
}
Ok(Some(out))
}
fn read_ancestor_defs(block: &mut Block<'_>) -> Result<Vec<PBmmGenericType>, PBmmReadError> {
let path = block.member_path("ancestor_defs");
let Some(value) = block.take("ancestor_defs") else {
return Ok(Vec::new());
};
let mut out = Vec::new();
for (key, entry) in keyed_entries(&path, value)? {
let entry_path = join_path(&path, &key_text(key));
out.push(read_generic_type(&entry_path, entry)?);
}
Ok(out)
}
fn read_constants(
block: &mut Block<'_>,
) -> Result<Option<BTreeMap<String, PBmmConstant>>, PBmmReadError> {
let path = block.member_path("constants");
let Some(value) = block.take("constants") else {
return Ok(None);
};
let empty = IndexMap::new();
let mut out = BTreeMap::new();
for (key, entry) in keyed_entries(&path, value)? {
let key = key_text(key);
let entry_path = join_path(&path, &key);
let mut entry_block = Block::open(&entry_path, entry, &empty)?;
let name = read_name(&mut entry_block, &key)?;
let documentation = entry_block.optional_string("documentation")?;
let r#type = entry_block.required_string("type")?;
let value = entry_block.literal_text("value")?;
entry_block.finish(&[])?;
out.insert(
key,
PBmmConstant {
documentation,
name,
r#type,
value,
},
);
}
Ok(Some(out))
}
fn read_functions(
block: &mut Block<'_>,
) -> Result<Option<BTreeMap<String, PBmmFunction>>, PBmmReadError> {
let path = block.member_path("functions");
let Some(value) = block.take("functions") else {
return Ok(None);
};
let empty = IndexMap::new();
let mut out = BTreeMap::new();
for (key, entry) in keyed_entries(&path, value)? {
let key = key_text(key);
let entry_path = join_path(&path, &key);
let mut entry_block = Block::open(&entry_path, entry, &empty)?;
let name = read_name(&mut entry_block, &key)?;
let documentation = entry_block.optional_string("documentation")?;
let aliases = read_aliases(&mut entry_block)?;
let is_abstract = entry_block.optional_bool("is_abstract")?;
let parameters = read_parameters(&mut entry_block)?;
let pre_conditions = entry_block.string_map("pre_conditions")?;
let post_conditions = entry_block.string_map("post_conditions")?;
let result_path = entry_block.member_path("result");
let result = match entry_block.take("result") {
Some(value) => Some(read_type(&result_path, value)?),
None => None,
};
let is_nullable = entry_block.optional_bool("is_nullable")?;
entry_block.finish(&[])?;
out.insert(
key,
PBmmFunction {
documentation,
name,
aliases,
is_abstract,
parameters,
pre_conditions,
post_conditions,
result,
is_nullable,
},
);
}
Ok(Some(out))
}
fn read_aliases(block: &mut Block<'_>) -> Result<Option<BTreeMap<String, String>>, PBmmReadError> {
let path = block.member_path("aliases");
let Some(value) = block.take("aliases") else {
return Ok(None);
};
if matches!(value, OdinValue::KeyedList(_)) {
let mut out = BTreeMap::new();
for (key, entry) in keyed_entries(&path, value)? {
let key = key_text(key);
let entry_path = join_path(&path, &key);
let text = as_string(&entry_path, entry)?;
out.insert(key, text);
}
return Ok(Some(out));
}
let aliases = string_list_of(&path, value)?;
Ok(Some(
aliases
.into_iter()
.map(|alias| (alias.clone(), alias))
.collect(),
))
}
fn read_parameters(
block: &mut Block<'_>,
) -> Result<Option<BTreeMap<String, PBmmFunctionParameter>>, PBmmReadError> {
let path = block.member_path("parameters");
let Some(value) = block.take("parameters") else {
return Ok(None);
};
let mut out = BTreeMap::new();
for (key, entry) in keyed_entries(&path, value)? {
let key = key_text(key);
let entry_path = join_path(&path, &key);
out.insert(key.clone(), read_parameter(&entry_path, &key, entry)?);
}
Ok(Some(out))
}
fn read_parameter(
path: &str,
key: &str,
value: &OdinValue,
) -> Result<PBmmFunctionParameter, PBmmReadError> {
let (marker, body) = split_marker(value);
let empty = IndexMap::new();
let mut block = Block::open(path, body, &empty)?;
let name = read_name(&mut block, key)?;
let documentation = block.optional_string("documentation")?;
let is_nullable = block.optional_bool("is_nullable")?;
let parameter = match marker {
Some("P_BMM_SINGLE_FUNCTION_PARAMETER") => {
let r#type = block.required_string("type")?;
block.finish(&[])?;
PBmmFunctionParameter::PBmmSingleFunctionParameter(PBmmSingleFunctionParameter {
documentation,
name,
is_nullable,
r#type,
})
}
Some("P_BMM_SINGLE_FUNCTION_PARAMETER_OPEN") => {
let r#type = block.required_string("type")?;
block.finish(&[])?;
PBmmFunctionParameter::PBmmSingleFunctionParameterOpen(
PBmmSingleFunctionParameterOpen {
documentation,
name,
is_nullable,
r#type,
},
)
}
Some("P_BMM_CONTAINER_FUNCTION_PARAMETER") => {
let type_def = read_required_container_type(&mut block)?;
let cardinality = block.cardinality()?;
block.finish(&[])?;
PBmmFunctionParameter::PBmmContainerFunctionParameter(PBmmContainerFunctionParameter {
documentation,
name,
is_nullable,
type_def,
cardinality,
})
}
Some("P_BMM_GENERIC_FUNCTION_PARAMETER") => {
let type_def_path = block.member_path("type_def");
let Some(type_def) = block.take("type_def") else {
return Err(PBmmReadError::MissingAttribute {
path: path.to_owned(),
attribute: "type_def",
});
};
let type_def = read_generic_type(&type_def_path, type_def)?;
block.finish(&[])?;
PBmmFunctionParameter::PBmmGenericFunctionParameter(PBmmGenericFunctionParameter {
documentation,
name,
is_nullable,
type_def,
})
}
Some(other) => {
return Err(PBmmReadError::UnexpectedTypeMarker {
path: path.to_owned(),
marker: other.to_owned(),
expected: "P_BMM_FUNCTION_PARAMETER",
});
}
None => {
return Err(PBmmReadError::MissingTypeMarker {
path: path.to_owned(),
expected: "P_BMM_FUNCTION_PARAMETER",
});
}
};
Ok(parameter)
}
fn read_required_container_type(block: &mut Block<'_>) -> Result<PBmmContainerType, PBmmReadError> {
let path = block.member_path("type_def");
match block.take("type_def") {
Some(value) => read_container_type(&path, value),
None => Err(PBmmReadError::MissingAttribute {
path: block.path.clone(),
attribute: "type_def",
}),
}
}
fn read_properties(
block: &mut Block<'_>,
) -> Result<Option<BTreeMap<String, PBmmProperty>>, PBmmReadError> {
let path = block.member_path("properties");
let Some(value) = block.take("properties") else {
return Ok(None);
};
let mut out = BTreeMap::new();
for (key, entry) in keyed_entries(&path, value)? {
let key = key_text(key);
let entry_path = join_path(&path, &key);
out.insert(key.clone(), read_property(&entry_path, &key, entry)?);
}
Ok(Some(out))
}
struct PropertyParts {
documentation: Option<String>,
name: String,
is_mandatory: Option<bool>,
is_computed: Option<bool>,
is_im_infrastructure: Option<bool>,
is_im_runtime: Option<bool>,
}
#[expect(
clippy::too_many_lines,
reason = "one arm per P_BMM_PROPERTY subtype; splitting the dispatch would hide the five-way marker → generated-struct mapping master04 §Class properties defines"
)]
fn read_property(path: &str, key: &str, value: &OdinValue) -> Result<PBmmProperty, PBmmReadError> {
let (marker, body) = split_marker(value);
let empty = IndexMap::new();
let mut block = Block::open(path, body, &empty)?;
let parts = PropertyParts {
name: read_name(&mut block, key)?,
documentation: block.optional_string("documentation")?,
is_mandatory: block.optional_bool("is_mandatory")?,
is_computed: block.optional_bool("is_computed")?,
is_im_infrastructure: block.optional_bool("is_im_infrastructure")?,
is_im_runtime: block.optional_bool("is_im_runtime")?,
};
let property = match marker {
Some("P_BMM_SINGLE_PROPERTY") => {
let r#type = block.optional_string("type")?;
let simple_ref = read_optional_simple_type(&mut block)?;
let structural = read_optional_type(&mut block)?;
block.finish(TOLERATED_PROPERTY_ATTRIBUTES)?;
PBmmProperty::PBmmSingleProperty(PBmmSingleProperty {
documentation: parts.documentation,
name: parts.name,
is_mandatory: parts.is_mandatory,
is_computed: parts.is_computed,
is_im_infrastructure: parts.is_im_infrastructure,
is_im_runtime: parts.is_im_runtime,
type_def: structural,
bmm_property: None,
r#type,
type_ref: simple_ref,
})
}
Some("P_BMM_SINGLE_PROPERTY_OPEN") => {
let r#type = block.optional_string("type")?;
let open_ref = read_optional_open_type(&mut block)?;
let structural = read_optional_type(&mut block)?;
block.finish(TOLERATED_PROPERTY_ATTRIBUTES)?;
PBmmProperty::PBmmSinglePropertyOpen(PBmmSinglePropertyOpen {
documentation: parts.documentation,
name: parts.name,
is_mandatory: parts.is_mandatory,
is_computed: parts.is_computed,
is_im_infrastructure: parts.is_im_infrastructure,
is_im_runtime: parts.is_im_runtime,
type_def: structural,
bmm_property: None,
type_ref: open_ref,
r#type,
})
}
Some("P_BMM_GENERIC_PROPERTY") => {
let type_def_path = block.member_path("type_def");
let type_def = match block.take("type_def") {
Some(value) => Some(read_generic_type(&type_def_path, value)?),
None => None,
};
block.finish(TOLERATED_PROPERTY_ATTRIBUTES)?;
PBmmProperty::PBmmGenericProperty(PBmmGenericProperty {
documentation: parts.documentation,
name: parts.name,
is_mandatory: parts.is_mandatory,
is_computed: parts.is_computed,
is_im_infrastructure: parts.is_im_infrastructure,
is_im_runtime: parts.is_im_runtime,
type_def,
bmm_property: None,
})
}
Some("P_BMM_CONTAINER_PROPERTY") => {
let type_def_path = block.member_path("type_def");
let type_def = match block.take("type_def") {
Some(value) => Some(read_container_type(&type_def_path, value)?),
None => None,
};
let cardinality = block.cardinality()?;
block.finish(TOLERATED_PROPERTY_ATTRIBUTES)?;
PBmmProperty::PBmmContainerProperty(PBmmContainerProperty::PBmmContainerProperty(
PBmmContainerPropertyData {
documentation: parts.documentation,
name: parts.name,
is_mandatory: parts.is_mandatory,
is_computed: parts.is_computed,
is_im_infrastructure: parts.is_im_infrastructure,
is_im_runtime: parts.is_im_runtime,
type_def,
bmm_property: None,
cardinality,
},
))
}
Some("P_BMM_INDEXED_CONTAINER_PROPERTY") => {
let type_def_path = block.member_path("type_def");
let type_def = match block.take("type_def") {
Some(value) => Some(read_indexed_container_type(&type_def_path, value)?),
None => None,
};
let cardinality = block.cardinality()?;
block.finish(TOLERATED_PROPERTY_ATTRIBUTES)?;
PBmmProperty::PBmmContainerProperty(
PBmmContainerProperty::PBmmIndexedContainerProperty(PBmmIndexedContainerProperty {
documentation: parts.documentation,
name: parts.name,
is_mandatory: parts.is_mandatory,
is_computed: parts.is_computed,
is_im_infrastructure: parts.is_im_infrastructure,
is_im_runtime: parts.is_im_runtime,
type_def,
bmm_property: None,
cardinality,
}),
)
}
Some(other) => {
return Err(PBmmReadError::UnexpectedTypeMarker {
path: path.to_owned(),
marker: other.to_owned(),
expected: "P_BMM_PROPERTY",
});
}
None => {
return Err(PBmmReadError::MissingTypeMarker {
path: path.to_owned(),
expected: "P_BMM_PROPERTY",
});
}
};
Ok(property)
}
fn read_optional_type(block: &mut Block<'_>) -> Result<Option<PBmmType>, PBmmReadError> {
let path = block.member_path("type_def");
match block.take("type_def") {
Some(value) => read_type(&path, value).map(Some),
None => Ok(None),
}
}
fn read_optional_simple_type(
block: &mut Block<'_>,
) -> Result<Option<PBmmSimpleType>, PBmmReadError> {
let path = block.member_path("type_ref");
match block.take("type_ref") {
Some(value) => read_simple_type(&path, value).map(Some),
None => Ok(None),
}
}
fn read_optional_open_type(block: &mut Block<'_>) -> Result<Option<PBmmOpenType>, PBmmReadError> {
let path = block.member_path("type_ref");
match block.take("type_ref") {
Some(value) => read_open_type(&path, value).map(Some),
None => Ok(None),
}
}
fn read_type(path: &str, value: &OdinValue) -> Result<PBmmType, PBmmReadError> {
let (marker, body) = split_marker(value);
match marker {
Some("P_BMM_SIMPLE_TYPE") => read_simple_type(path, value).map(PBmmType::PBmmSimpleType),
Some("P_BMM_OPEN_TYPE") => read_open_type(path, value).map(PBmmType::PBmmOpenType),
Some("P_BMM_GENERIC_TYPE") => read_generic_type(path, value).map(PBmmType::PBmmGenericType),
Some("P_BMM_CONTAINER_TYPE" | "P_BMM_INDEXED_CONTAINER_TYPE") => {
read_container_type(path, value).map(PBmmType::PBmmContainerType)
}
Some(other) => Err(PBmmReadError::UnexpectedTypeMarker {
path: path.to_owned(),
marker: other.to_owned(),
expected: "P_BMM_TYPE",
}),
None => match infer_type_kind(body) {
Some(TypeKind::Container | TypeKind::IndexedContainer) => {
read_container_type(path, value).map(PBmmType::PBmmContainerType)
}
Some(TypeKind::Generic) => {
read_generic_type(path, value).map(PBmmType::PBmmGenericType)
}
Some(TypeKind::Simple) => read_simple_type(path, value).map(PBmmType::PBmmSimpleType),
None => Err(PBmmReadError::MissingTypeMarker {
path: path.to_owned(),
expected: "P_BMM_TYPE",
}),
},
}
}
fn read_base_type(path: &str, value: &OdinValue) -> Result<PBmmBaseType, PBmmReadError> {
let (marker, body) = split_marker(value);
match marker {
Some("P_BMM_SIMPLE_TYPE") => {
read_simple_type(path, value).map(PBmmBaseType::PBmmSimpleType)
}
Some("P_BMM_OPEN_TYPE") => read_open_type(path, value).map(PBmmBaseType::PBmmOpenType),
Some("P_BMM_GENERIC_TYPE") => {
read_generic_type(path, value).map(PBmmBaseType::PBmmGenericType)
}
Some(other) => Err(PBmmReadError::UnexpectedTypeMarker {
path: path.to_owned(),
marker: other.to_owned(),
expected: "P_BMM_BASE_TYPE",
}),
None => match infer_type_kind(body) {
Some(TypeKind::Generic) => {
read_generic_type(path, value).map(PBmmBaseType::PBmmGenericType)
}
Some(TypeKind::Simple) => {
read_simple_type(path, value).map(PBmmBaseType::PBmmSimpleType)
}
Some(TypeKind::Container | TypeKind::IndexedContainer) | None => {
Err(PBmmReadError::MissingTypeMarker {
path: path.to_owned(),
expected: "P_BMM_BASE_TYPE",
})
}
},
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TypeKind {
IndexedContainer,
Container,
Generic,
Simple,
}
fn infer_type_kind(body: &OdinValue) -> Option<TypeKind> {
let OdinValue::Object(members) = body else {
return None;
};
if members.contains_key("container_type") {
if members.contains_key("index_type") {
return Some(TypeKind::IndexedContainer);
}
return Some(TypeKind::Container);
}
if members.contains_key("root_type") {
return Some(TypeKind::Generic);
}
if members.contains_key("type") {
return Some(TypeKind::Simple);
}
None
}
fn read_simple_type(path: &str, value: &OdinValue) -> Result<PBmmSimpleType, PBmmReadError> {
let body = expect_marker(path, value, "P_BMM_SIMPLE_TYPE", &["P_BMM_SIMPLE_TYPE"])?;
let empty = IndexMap::new();
let mut block = Block::open(path, body, &empty)?;
let r#type = block.required_string("type")?;
let value_constraint = block.optional_string("value_constraint")?;
block.finish(&[])?;
Ok(PBmmSimpleType {
bmm_type: None,
value_constraint,
r#type,
})
}
fn read_open_type(path: &str, value: &OdinValue) -> Result<PBmmOpenType, PBmmReadError> {
let body = expect_marker(path, value, "P_BMM_OPEN_TYPE", &["P_BMM_OPEN_TYPE"])?;
let empty = IndexMap::new();
let mut block = Block::open(path, body, &empty)?;
let r#type = block.required_string("type")?;
let value_constraint = block.optional_string("value_constraint")?;
block.finish(&[])?;
Ok(PBmmOpenType {
bmm_type: None,
value_constraint,
r#type,
})
}
fn read_generic_type(path: &str, value: &OdinValue) -> Result<PBmmGenericType, PBmmReadError> {
let body = expect_marker(path, value, "P_BMM_GENERIC_TYPE", &["P_BMM_GENERIC_TYPE"])?;
let empty = IndexMap::new();
let mut block = Block::open(path, body, &empty)?;
let root_type = block.required_string("root_type")?;
let (generic_parameters, mut generic_parameter_defs) = read_generic_parameters(&mut block)?;
let defs_path = block.member_path("generic_parameter_defs");
if let Some(defs) = block.take("generic_parameter_defs") {
for (key, entry) in keyed_entries(&defs_path, defs)? {
let entry_path = join_path(&defs_path, &key_text(key));
generic_parameter_defs.push(read_type(&entry_path, entry)?);
}
}
let value_constraint = block.optional_string("value_constraint")?;
block.finish(&[])?;
Ok(PBmmGenericType {
bmm_type: None,
value_constraint,
root_type,
generic_parameter_defs,
generic_parameters: present(generic_parameters),
})
}
fn read_generic_parameters(
block: &mut Block<'_>,
) -> Result<(Vec<String>, Vec<PBmmType>), PBmmReadError> {
let path = block.member_path("generic_parameters");
let Some(value) = block.take("generic_parameters") else {
return Ok((Vec::new(), Vec::new()));
};
if !matches!(value, OdinValue::KeyedList(_)) {
return Ok((string_list_of(&path, value)?, Vec::new()));
}
let mut defs = Vec::new();
for (key, entry) in keyed_entries(&path, value)? {
let entry_path = join_path(&path, &key_text(key));
match entry {
OdinValue::String(name) => defs.push(PBmmType::PBmmSimpleType(PBmmSimpleType {
bmm_type: None,
value_constraint: None,
r#type: name.clone(),
})),
structured => defs.push(read_type(&entry_path, structured)?),
}
}
Ok((Vec::new(), defs))
}
fn read_container_type(path: &str, value: &OdinValue) -> Result<PBmmContainerType, PBmmReadError> {
let (marker, body) = split_marker(value);
match marker {
Some("P_BMM_INDEXED_CONTAINER_TYPE") => read_indexed_container_type(path, value)
.map(PBmmContainerType::PBmmIndexedContainerType),
Some("P_BMM_CONTAINER_TYPE") | None => {
if infer_type_kind(body) == Some(TypeKind::IndexedContainer) {
return read_indexed_container_type(path, value)
.map(PBmmContainerType::PBmmIndexedContainerType);
}
let empty = IndexMap::new();
let mut block = Block::open(path, body, &empty)?;
let container_type = block.required_string("container_type")?;
let r#type = block.optional_string("type")?;
let type_def = read_optional_base_type(&mut block)?;
block.finish(&[])?;
Ok(PBmmContainerType::PBmmContainerType(
PBmmContainerTypeData {
bmm_type: None,
container_type,
type_def,
r#type,
},
))
}
Some(other) => Err(PBmmReadError::UnexpectedTypeMarker {
path: path.to_owned(),
marker: other.to_owned(),
expected: "P_BMM_CONTAINER_TYPE",
}),
}
}
fn read_indexed_container_type(
path: &str,
value: &OdinValue,
) -> Result<PBmmIndexedContainerType, PBmmReadError> {
let body = expect_marker(
path,
value,
"P_BMM_INDEXED_CONTAINER_TYPE",
&["P_BMM_INDEXED_CONTAINER_TYPE"],
)?;
let empty = IndexMap::new();
let mut block = Block::open(path, body, &empty)?;
let container_type = block.required_string("container_type")?;
let index_type = block.required_string("index_type")?;
let r#type = block.optional_string("type")?;
let type_def = read_optional_base_type(&mut block)?;
block.finish(&[])?;
Ok(PBmmIndexedContainerType {
bmm_type: None,
container_type,
type_def,
r#type,
index_type,
})
}
fn read_optional_base_type(block: &mut Block<'_>) -> Result<Option<PBmmBaseType>, PBmmReadError> {
let path = block.member_path("type_def");
match block.take("type_def") {
Some(value) => read_base_type(&path, value).map(Some),
None => Ok(None),
}
}
fn expect_marker<'a>(
path: &str,
value: &'a OdinValue,
expected: &'static str,
allowed: &[&str],
) -> Result<&'a OdinValue, PBmmReadError> {
let (marker, body) = split_marker(value);
match marker {
None => Ok(body),
Some(found) if allowed.contains(&found) => Ok(body),
Some(found) => Err(PBmmReadError::UnexpectedTypeMarker {
path: path.to_owned(),
marker: found.to_owned(),
expected,
}),
}
}
#[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 super::read_schema;
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_container_property::PBmmContainerProperty;
use crate::v1_1::bmm_persistence::p_bmm_container_type::PBmmContainerType;
use crate::v1_1::bmm_persistence::p_bmm_generic_type::PBmmGenericType;
use crate::v1_1::bmm_persistence::p_bmm_indexed_container_type::PBmmIndexedContainerType;
use crate::v1_1::bmm_persistence::p_bmm_property::PBmmProperty;
use crate::v1_1::bmm_persistence::p_bmm_schema::PBmmSchema;
use crate::v1_1::bmm_persistence::p_bmm_type::PBmmType;
use openehr_base::v1_3::prelude::Interval;
use openehr_base::v1_3::prelude::ProperInterval;
const HEADER: &str = r#"
bmm_version = <"2.4">
rm_publisher = <"openehr">
schema_name = <"adltest">
rm_release = <"1.0.2">
"#;
fn read(body: &str) -> Result<PBmmSchema, PBmmReadError> {
read_schema(&format!("{HEADER}{body}"))
}
fn class<'a>(schema: &'a PBmmSchema, name: &str) -> &'a PBmmClass {
schema
.class_definitions
.iter()
.flatten()
.find(|class| class.name() == name)
.expect("the fixture defines the class")
}
fn property<'a>(schema: &'a PBmmSchema, class_name: &str, property: &str) -> &'a PBmmProperty {
class(schema, class_name)
.properties()
.expect("the class declares properties")
.get(property)
.expect("the class declares the property")
}
#[test]
fn header_items_read_verbatim() -> Result<(), PBmmReadError> {
let schema = read_schema(
r#"
bmm_version = <"2.3">
rm_publisher = <"openehr">
schema_name = <"adltest">
rm_release = <"1.0.2">
model_name = <"TEST_PKG">
schema_revision = <"1.0.36">
schema_lifecycle_state = <"stable">
schema_description = <"openEHR schema to support test archetypes">
"#,
)?;
assert_eq!(schema.bmm_version, "2.3");
assert_eq!(schema.schema_id(), "openehr_adltest_1.0.2");
assert_eq!(schema.schema_revision, "1.0.36");
assert_eq!(schema.schema_lifecycle_state, "stable");
assert_eq!(schema.schema_author, "");
Ok(())
}
#[test]
fn a_missing_identifying_header_is_refused() {
let error = read_schema(r#"bmm_version = <"2.4">"#).expect_err("rm_publisher is missing");
assert_eq!(
error,
PBmmReadError::MissingAttribute {
path: String::new(),
attribute: "rm_publisher",
}
);
}
#[test]
fn an_undeclared_attribute_is_refused_by_name() {
let error = read(r#"not_a_bmm_attribute = <"x">"#).expect_err("the attribute is unknown");
assert_eq!(
error,
PBmmReadError::UnknownAttribute {
path: String::new(),
attribute: "not_a_bmm_attribute".to_owned(),
}
);
}
#[test]
fn includes_are_keyed_by_their_own_id() -> Result<(), PBmmReadError> {
let schema = read(
r#"
includes = <
["1"] = <
id = <"openehr_basic_types_1.0.2">
>
>
"#,
)?;
let includes = schema.includes.expect("the schema declares an include");
assert_eq!(includes.len(), 1);
assert_eq!(
includes
.get("openehr_basic_types_1.0.2")
.map(|spec| spec.id.as_str()),
Some("openehr_basic_types_1.0.2")
);
Ok(())
}
#[test]
fn packages_read_recursively_and_only_top_level_ids_may_be_paths() -> Result<(), PBmmReadError>
{
let schema = read(
r#"
packages = <
["org.openehr.test_pkg"] = <
name = <"org.openehr.test_pkg">
classes = <"WHOLE", "SOME_TYPE", "BOOK">
>
>
"#,
)?;
let package = schema
.packages
.get("org.openehr.test_pkg")
.expect("the top-level package reads");
assert_eq!(package.classes.as_ref().map_or(0, Vec::len), 3);
let error = read(
r#"
packages = <
["ParentPackage"] = <
name = <"ParentPackage">
packages = <
["invalid.ChildPackage"] = <
name = <"invalid.ChildPackage">
>
>
>
>
"#,
)
.expect_err("a nested package id may not be a path");
assert!(
matches!(error, PBmmReadError::QualifiedNestedPackage { .. }),
"expected a qualified-nested-package refusal, got {error:?}"
);
Ok(())
}
#[test]
fn a_key_that_disagrees_with_the_name_attribute_is_refused() {
let error = read(
r#"
packages = <
["ParentPackage"] = <
name = <"OtherName">
>
>
"#,
)
.expect_err("the key and name disagree");
assert!(
matches!(error, PBmmReadError::KeyNameMismatch { .. }),
"expected a key/name refusal, got {error:?}"
);
}
#[test]
fn single_and_container_properties_read_the_master04_examples() -> Result<(), PBmmReadError> {
let schema = read(
r#"
class_definitions = <
["ELEMENT"] = <
name = <"ELEMENT">
ancestors = <"ITEM">
properties = <
["null_flavour"] = (P_BMM_SINGLE_PROPERTY) <
name = <"null_flavour">
type = <"DV_CODED_TEXT">
is_mandatory = <True>
>
["items"] = (P_BMM_CONTAINER_PROPERTY) <
name = <"items">
type_def = <
container_type = <"List">
type = <"ITEM">
>
cardinality = <|>=1|>
is_mandatory = <True>
>
["custom_actions"] = (P_BMM_INDEXED_CONTAINER_PROPERTY) <
name = <"custom_actions">
type_def = <
container_type = <"Hash">
index_type = <"String">
type = <"EVENT_ACTION">
>
cardinality = <|>=0|>
>
>
>
>
"#,
)?;
assert_eq!(class(&schema, "ELEMENT").ancestors(), ["ITEM"]);
assert_eq!(class(&schema, "ELEMENT").uid(), Some(1));
assert_eq!(
class(&schema, "ELEMENT").source_schema_id(),
Some("openehr_adltest_1.0.2")
);
let PBmmProperty::PBmmSingleProperty(single) = property(&schema, "ELEMENT", "null_flavour")
else {
panic!("null_flavour is a P_BMM_SINGLE_PROPERTY");
};
assert_eq!(single.r#type.as_deref(), Some("DV_CODED_TEXT"));
assert_eq!(single.is_mandatory, Some(true));
let PBmmProperty::PBmmContainerProperty(PBmmContainerProperty::PBmmContainerProperty(
container,
)) = property(&schema, "ELEMENT", "items")
else {
panic!("items is a P_BMM_CONTAINER_PROPERTY");
};
let type_def = container
.type_def
.as_ref()
.expect("the container states a type_def");
assert_eq!(type_def.as_type_string(), "List<ITEM>");
let Some(Interval::ProperInterval(ProperInterval::ProperInterval(cardinality))) =
container.cardinality.as_ref()
else {
panic!("the cardinality reads as a proper interval");
};
assert_eq!(cardinality.lower, Some(1));
assert!(cardinality.lower_included);
assert_eq!(cardinality.upper, None);
assert!(cardinality.upper_unbounded);
let PBmmProperty::PBmmContainerProperty(
PBmmContainerProperty::PBmmIndexedContainerProperty(indexed),
) = property(&schema, "ELEMENT", "custom_actions")
else {
panic!("custom_actions is a P_BMM_INDEXED_CONTAINER_PROPERTY");
};
assert_eq!(
indexed
.type_def
.as_ref()
.map(PBmmIndexedContainerType::as_type_string)
.as_deref(),
Some("Hash<String,EVENT_ACTION>")
);
Ok(())
}
#[test]
fn a_property_without_a_type_marker_is_refused() {
let error = read(
r#"
class_definitions = <
["ELEMENT"] = <
name = <"ELEMENT">
properties = <
["value"] = <
name = <"value">
type = <"DATA_VALUE">
>
>
>
>
"#,
)
.expect_err("§Class properties requires the marker");
assert_eq!(
error,
PBmmReadError::MissingTypeMarker {
path: "class_definitions/ELEMENT/properties/value".to_owned(),
expected: "P_BMM_PROPERTY",
}
);
}
#[test]
fn crazy_type_reads_every_mixed_generic_parameter_form() -> Result<(), PBmmReadError> {
let schema = read(
r#"
class_definitions = <
["CRAZY_TYPE"] = <
name = <"CRAZY_TYPE">
ancestors = <"Any">
properties = <
["range"] = (P_BMM_GENERIC_PROPERTY) <
name = <"range">
type_def = <
root_type = <"REFERENCE_RANGE">
generic_parameter_defs = <
["T"] = (P_BMM_GENERIC_TYPE) <
root_type = <"DV_INTERVAL">
generic_parameters = <"DV_QUANTITY">
>
["U"] = (P_BMM_SIMPLE_TYPE) <
type = <"Integer">
>
["V"] = (P_BMM_CONTAINER_TYPE) <
type = <"DV_QUANTITY">
container_type = <"List">
>
["W"] = (P_BMM_CONTAINER_TYPE) <
type_def = (P_BMM_GENERIC_TYPE) <
root_type = <"DV_INTERVAL">
generic_parameters = <"DV_QUANTITY">
>
container_type = <"List">
>
>
>
>
>
>
>
"#,
)?;
let PBmmProperty::PBmmGenericProperty(generic) = property(&schema, "CRAZY_TYPE", "range")
else {
panic!("range is a P_BMM_GENERIC_PROPERTY");
};
let type_def = generic.type_def.as_ref().expect("range states a type_def");
assert_eq!(
type_def.as_type_string(),
"REFERENCE_RANGE<DV_INTERVAL<DV_QUANTITY>,Integer,List<DV_QUANTITY>,List<DV_INTERVAL<DV_QUANTITY>>>"
);
Ok(())
}
#[test]
fn value_set_constraints_read_in_both_positions() -> Result<(), PBmmReadError> {
let schema = read(
r#"
class_definitions = <
["TRANSLATION_DETAILS"] = <
name = <"TRANSLATION_DETAILS">
properties = <
["encoding"] = (P_BMM_SINGLE_PROPERTY) <
name = <"encoding">
type_ref = <
type = <"CODE_PHRASE">
value_constraint = <"openEHR::languages">
>
>
["language"] = (P_BMM_CONTAINER_PROPERTY) <
name = <"language">
type_def = <
container_type = <"List">
type_def = (P_BMM_SIMPLE_TYPE) <
type = <"Coding">
value_constraint = <"hl7::Languages">
>
>
>
>
>
>
"#,
)?;
let PBmmProperty::PBmmSingleProperty(single) =
property(&schema, "TRANSLATION_DETAILS", "encoding")
else {
panic!("encoding is a P_BMM_SINGLE_PROPERTY");
};
let type_ref = single
.type_ref
.as_ref()
.expect("encoding states a type_ref");
assert_eq!(type_ref.r#type, "CODE_PHRASE");
assert_eq!(
type_ref.value_constraint.as_deref(),
Some("openEHR::languages")
);
let PBmmProperty::PBmmContainerProperty(PBmmContainerProperty::PBmmContainerProperty(
container,
)) = property(&schema, "TRANSLATION_DETAILS", "language")
else {
panic!("language is a P_BMM_CONTAINER_PROPERTY");
};
assert_eq!(
container
.type_def
.as_ref()
.map(PBmmContainerType::as_type_string)
.as_deref(),
Some("List<Coding>")
);
Ok(())
}
#[test]
fn functions_constants_and_invariants_read() -> Result<(), PBmmReadError> {
let schema = read(
r#"
class_definitions = <
["BMM_MODEL"] = <
name = <"BMM_MODEL">
ancestors = <"BMM_PACKAGE_CONTAINER">
functions = <
["class_definition"] = <
name = <"class_definition">
parameters = <
["a_name"] = (P_BMM_SINGLE_FUNCTION_PARAMETER) <
name = <"a_name">
type = <"String">
>
>
result = (P_BMM_SIMPLE_TYPE) <
type = <"BMM_CLASS">
>
>
>
>
["OPENEHR_DEFINITIONS"] = <
name = <"OPENEHR_DEFINITIONS">
constants = <
["Local_terminology_id"] = <
name = <"Local_terminology_id">
type = <"String">
value = <"local">
>
>
>
["LOCATABLE"] = <
name = <"LOCATABLE">
invariants = <
["Links_valid"] = <"links /= Void implies not links.is_empty">
["Archetype_node_id_valid"] = <"not archetype_node_id.is_empty">
>
>
>
"#,
)?;
let functions = class(&schema, "BMM_MODEL")
.functions()
.expect("BMM_MODEL declares functions");
let function = functions
.get("class_definition")
.expect("class_definition reads");
assert_eq!(
function
.result
.as_ref()
.map(PBmmType::as_type_string)
.as_deref(),
Some("BMM_CLASS")
);
assert_eq!(
function
.parameters
.as_ref()
.map(std::collections::BTreeMap::len),
Some(1)
);
let constants = class(&schema, "OPENEHR_DEFINITIONS")
.constants()
.expect("OPENEHR_DEFINITIONS declares constants");
assert_eq!(
constants
.get("Local_terminology_id")
.and_then(|constant| constant.value.as_deref()),
Some("local")
);
let invariants = class(&schema, "LOCATABLE")
.invariants()
.expect("LOCATABLE declares invariants");
assert_eq!(invariants.len(), 2);
assert_eq!(
invariants.get("Links_valid").map(String::as_str),
Some("links /= Void implies not links.is_empty")
);
Ok(())
}
#[test]
fn enumerations_read_names_and_values() -> Result<(), PBmmReadError> {
let schema = read(
r#"
class_definitions = <
["PROPORTION_KIND_2"] = (P_BMM_ENUMERATION_INTEGER) <
name = <"PROPORTION_KIND_2">
ancestors = <"Integer">
item_names = <"pk_ratio", "pk_unitary", "pk_percent", "pk_fraction">
item_values = <0, 1001, 1002, 1003>
>
["MAGNITUDE_STATUS"] = (P_BMM_ENUMERATION_STRING) <
name = <"MAGNITUDE_STATUS">
ancestors = <"String", ...>
item_names = <"le", "ge", "eq", "approx_eq">
item_values = <"<=", ">=", "=", "~">
>
>
"#,
)?;
let PBmmClass::PBmmEnumeration(integer) = class(&schema, "PROPORTION_KIND_2") else {
panic!("PROPORTION_KIND_2 is a P_BMM_ENUMERATION_INTEGER");
};
assert_eq!(integer.item_names().len(), 4);
assert_eq!(integer.item_values().len(), 4);
assert_eq!(integer.underlying_type_name(), "INTEGER");
let PBmmClass::PBmmEnumeration(string) = class(&schema, "MAGNITUDE_STATUS") else {
panic!("MAGNITUDE_STATUS is a P_BMM_ENUMERATION_STRING");
};
assert_eq!(string.underlying_type_name(), "STRING");
assert_eq!(class(&schema, "MAGNITUDE_STATUS").ancestors(), ["String"]);
Ok(())
}
#[test]
fn generic_inheritance_reads_ancestor_defs() -> Result<(), PBmmReadError> {
let schema = read(
r#"
class_definitions = <
["GENERIC_CHILD_OPEN_T"] = <
name = <"GENERIC_CHILD_OPEN_T">
ancestor_defs = <
["GENERIC_PARENT<T,SUPPLIER_B>"] = (P_BMM_GENERIC_TYPE) <
root_type = <"GENERIC_PARENT">
generic_parameters = <"T", "SUPPLIER_B">
>
>
generic_parameter_defs = <
["T"] = <
name = <"T">
conforms_to_type = <"SUPPLIER">
>
>
properties = <
["gen_child_open_t_prop"] = (P_BMM_SINGLE_PROPERTY) <
name = <"gen_child_open_t_prop">
type = <"String">
>
>
>
>
"#,
)?;
let child = class(&schema, "GENERIC_CHILD_OPEN_T");
assert!(child.is_generic());
let defs = child.ancestor_defs();
assert_eq!(defs.len(), 1);
let signature = defs
.first()
.map(PBmmGenericType::as_type_string)
.expect("the ancestor_defs entry reads");
assert_eq!(signature, "GENERIC_PARENT<T,SUPPLIER_B>");
Ok(())
}
#[test]
fn an_ancestor_def_that_is_not_a_generic_type_is_refused() {
let error = read(
r#"
class_definitions = <
["ParentType1"] = <
name = <"ParentType1">
ancestor_defs = <
["UNKNOWN"] = (P_BMM_SIMPLE_TYPE) <
type = <"UNKNOWN">
>
>
>
>
"#,
)
.expect_err("ancestor_defs admits only P_BMM_GENERIC_TYPE");
assert_eq!(
error,
PBmmReadError::UnexpectedTypeMarker {
path: "class_definitions/ParentType1/ancestor_defs/UNKNOWN".to_owned(),
marker: "P_BMM_SIMPLE_TYPE".to_owned(),
expected: "P_BMM_GENERIC_TYPE",
}
);
}
#[test]
fn a_persisted_interface_reads_its_name_documentation_and_functions()
-> Result<(), PBmmReadError> {
let schema = read(
r#"
class_definitions = <
["Math"] = (P_BMM_INTERFACE) <
name = <"Math">
documentation = <"Mathematical functions.">
functions = <
["abs"] = <
name = <"abs">
parameters = <
["v"] = (P_BMM_SINGLE_FUNCTION_PARAMETER) <
name = <"v">
type = <"Real">
>
>
result = (P_BMM_SIMPLE_TYPE) <
type = <"Real">
>
>
>
>
>
"#,
)?;
let interface = class(&schema, "Math");
assert!(matches!(interface, PBmmClass::PBmmInterface(_)));
assert_eq!(interface.documentation(), Some("Mathematical functions."));
let functions = interface.functions().expect("the declared functions");
assert_eq!(functions.keys().collect::<Vec<&String>>(), ["abs"]);
assert!(interface.properties().is_none());
assert!(interface.is_abstract());
assert_eq!(interface.source_schema_id(), None);
assert_eq!(interface.uid(), None);
Ok(())
}
#[test]
fn a_persisted_interface_refuses_a_state_carrying_attribute() {
let error = read(
r#"
class_definitions = <
["Env"] = (P_BMM_INTERFACE) <
name = <"Env">
properties = <
["home"] = (P_BMM_SINGLE_PROPERTY) <
name = <"home">
type = <"String">
>
>
>
>
"#,
)
.expect_err("an interface declares no properties");
assert_eq!(
error,
PBmmReadError::UnknownAttribute {
path: "class_definitions/Env".to_owned(),
attribute: "properties".to_owned(),
}
);
}
#[test]
fn primitive_types_read_as_ordinary_class_definitions() -> Result<(), PBmmReadError> {
let schema = read(
r#"
primitive_types = <
["Any"] = <
name = <"Any">
is_abstract = <True>
>
["Ordered"] = <
name = <"Ordered">
is_abstract = <True>
ancestors = <"Any">
>
>
"#,
)?;
assert_eq!(schema.primitive_types.as_ref().map_or(0, Vec::len), 2);
assert!(
schema
.primitive_types
.iter()
.flatten()
.all(PBmmClass::is_abstract)
);
assert_eq!(
schema
.primitive_types
.iter()
.flatten()
.map(PBmmClass::uid)
.collect::<Vec<Option<i32>>>(),
[Some(1), Some(2)]
);
Ok(())
}
}