#![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)?;
read_schema_tree(&root)
}
pub(super) fn read_schema_tree(root: &OdinValue) -> Result<PBmmSchema, PBmmReadError> {
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: (!primitive_types.is_empty()).then_some(primitive_types),
class_definitions: (!class_definitions.is_empty()).then_some(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(),
}
}
pub(super) 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 keyed_map<V>(
block: &mut Block<'_>,
name: &'static str,
mut read: impl FnMut(&str, &str, &OdinValue) -> Result<V, PBmmReadError>,
) -> Result<Option<BTreeMap<String, V>>, PBmmReadError> {
let path = block.member_path(name);
let Some(value) = block.take(name) else {
return Ok(None);
};
let mut out = BTreeMap::new();
for (key, entry) in keyed_entries(&path, value)? {
let key = key_text(key);
let read_entry = read(&join_path(&path, &key), &key, entry)?;
out.insert(key, read_entry);
}
Ok(Some(out))
}
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<BTreeMap<String, PBmmClass>, PBmmReadError> {
let path = block.member_path(name);
let Some(value) = block.take(name) else {
return Ok(BTreeMap::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);
*uid = uid.saturating_add(1);
let class = read_class(&entry_path, &key, entry, schema_id, *uid)?;
out.insert(key, class);
}
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<IndexMap<String, PBmmGenericParameter>>,
source_schema_id: String,
uid: i32,
ancestor_defs: Option<BTreeMap<String, PBmmBaseType>>,
}
struct EnumerationParts {
item_names: Option<Vec<String>>,
item_values: Option<Vec<serde_json::Value>>,
item_documentations: Option<Vec<String>>,
}
macro_rules! class_from_parts {
($ty:ident, $parts:expr, enumeration $items:expr) => {{
let items: EnumerationParts = $items;
class_from_parts!(
$ty,
$parts,
item_names: items.item_names,
item_values: items.item_values,
item_documentations: items.item_documentations,
)
}};
($ty:ident, $parts:expr $(, $field:ident $(: $value:expr)?)* $(,)?) => {{
let parts: ClassParts = $parts;
$ty {
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,
$($field $(: $value)?,)*
}
}};
}
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_from_parts!(PBmmClassData, parts))
}
Some("P_BMM_ENUMERATION") => {
let items = read_enumeration_parts(&mut block)?;
block.finish(&[])?;
PBmmClass::PBmmEnumeration(PBmmEnumeration::PBmmEnumeration(
class_from_parts!(PBmmEnumerationData, parts, enumeration items),
))
}
Some("P_BMM_ENUMERATION_INTEGER") => {
let items = read_enumeration_parts(&mut block)?;
block.finish(&[])?;
PBmmClass::PBmmEnumeration(PBmmEnumeration::PBmmEnumerationInteger(
class_from_parts!(PBmmEnumerationInteger, parts, enumeration items),
))
}
Some("P_BMM_ENUMERATION_STRING") => {
let items = read_enumeration_parts(&mut block)?;
block.finish(&[])?;
PBmmClass::PBmmEnumeration(PBmmEnumeration::PBmmEnumerationString(
class_from_parts!(PBmmEnumerationString, parts, enumeration 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: 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 read_generic_parameter_defs(
block: &mut Block<'_>,
) -> Result<Option<IndexMap<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 = IndexMap::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<Option<BTreeMap<String, PBmmBaseType>>, PBmmReadError> {
let defs = keyed_map(block, "ancestor_defs", |path, _, entry| {
read_base_type(path, entry)
})?;
Ok(defs.filter(|defs| !defs.is_empty()))
}
fn read_constants(
block: &mut Block<'_>,
) -> Result<Option<BTreeMap<String, PBmmConstant>>, PBmmReadError> {
let empty = IndexMap::new();
keyed_map(block, "constants", |entry_path, key, entry| {
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(&[])?;
Ok(PBmmConstant {
documentation,
name,
r#type,
value,
})
})
}
fn read_functions(
block: &mut Block<'_>,
) -> Result<Option<BTreeMap<String, PBmmFunction>>, PBmmReadError> {
let empty = IndexMap::new();
keyed_map(block, "functions", |entry_path, key, entry| {
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(&[])?;
Ok(PBmmFunction {
documentation,
name,
aliases,
is_abstract,
parameters,
pre_conditions,
post_conditions,
result,
is_nullable,
})
})
}
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> {
keyed_map(block, "parameters", read_parameter)
}
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> {
keyed_map(block, "properties", read_property)
}
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 key = key_text(key);
let entry_path = join_path(&defs_path, &key);
let def = read_type(&entry_path, entry)?;
generic_parameter_defs.insert(key, def);
}
}
let value_constraint = block.optional_string("value_constraint")?;
block.finish(&[])?;
Ok(PBmmGenericType {
bmm_type: None,
value_constraint,
root_type,
generic_parameter_defs: (!generic_parameter_defs.is_empty())
.then_some(generic_parameter_defs),
generic_parameters: present(generic_parameters),
})
}
fn read_generic_parameters(
block: &mut Block<'_>,
) -> Result<(Vec<String>, IndexMap<String, PBmmType>), PBmmReadError> {
let path = block.member_path("generic_parameters");
let Some(value) = block.take("generic_parameters") else {
return Ok((Vec::new(), IndexMap::new()));
};
if !matches!(value, OdinValue::KeyedList(_)) {
return Ok((string_list_of(&path, value)?, IndexMap::new()));
}
let mut defs = IndexMap::new();
for (key, entry) in keyed_entries(&path, value)? {
let key = key_text(key);
let entry_path = join_path(&path, &key);
let def = match entry {
OdinValue::String(name) => PBmmType::PBmmSimpleType(PBmmSimpleType {
bmm_type: None,
value_constraint: None,
r#type: name.clone(),
}),
structured => read_type(&entry_path, structured)?,
};
defs.insert(key, def);
}
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;