#![expect(
clippy::disallowed_types,
reason = "canonical JSON / Simplified Formats operate on the wire value by definition \
(ITS-JSON; ITS-REST Simplified Formats) — serde_json::Value IS the subject matter \
(#1694)"
)]
use std::collections::HashMap;
use indexmap::IndexMap;
use openehr_its::opt14::types::{
ArchetypeTerm, Assertion, CArchetypeRoot, CObject, CPrimitive, Cardinality,
Constraintbindingset, ExprItem, Intervalofinteger, OperationalTemplate, OperatorKind,
TermBindingItem, Termbindingset, ValidityKind,
};
use super::inputs::{self, Labels};
use super::model::{
CodedName, WebTemplate, WebTemplateArchetypeSlot, WebTemplateBindingCodedValue,
WebTemplateCardinality, WebTemplateClosedAttribute, WebTemplateCodeList,
WebTemplateConstraintBinding, WebTemplateExistence, WebTemplateNode, WebTemplateStructuralStub,
};
use super::shape;
#[derive(Default)]
struct Rubric {
text: Option<String>,
description: Option<String>,
}
type Ontology = HashMap<String, HashMap<String, HashMap<String, Rubric>>>;
type TermBindings = HashMap<String, Vec<Termbindingset>>;
type ConstraintBindings = HashMap<String, Vec<Constraintbindingset>>;
struct Ctx {
default_language: String,
languages: Vec<String>,
ontology: Ontology,
term_bindings: TermBindings,
constraint_bindings: ConstraintBindings,
}
impl Ctx {
fn rubric(&self, arch_id: &str, lang: &str, code: &str) -> Option<&Rubric> {
self.ontology.get(arch_id)?.get(lang)?.get(code)
}
fn text(&self, arch_id: &str, code: &str, lang: &str) -> Option<String> {
self.rubric(arch_id, lang, code)
.and_then(|r| r.text.clone())
}
fn term_bindings_for(
&self,
arch_id: &str,
code: &str,
) -> IndexMap<String, WebTemplateBindingCodedValue> {
let mut out = IndexMap::new();
if code.is_empty() {
return out;
}
let Some(sets) = self.term_bindings.get(arch_id) else {
return out;
};
for set in sets {
if out.contains_key(&set.terminology) {
continue;
}
if let Some(item) = set.items.iter().find(|it| it.code == code)
&& let Some(bcv) = binding_coded_value(item)
{
out.insert(set.terminology.clone(), bcv);
}
}
out
}
fn constraint_bindings_for(
&self,
arch_id: &str,
ac_code: &str,
attr: &str,
) -> Vec<WebTemplateConstraintBinding> {
let mut out = Vec::new();
if ac_code.is_empty() {
return out;
}
let Some(sets) = self.constraint_bindings.get(arch_id) else {
return out;
};
for set in sets {
for item in set.items.iter().filter(|it| it.code == ac_code) {
let query_uri = item.value.trim();
if query_uri.is_empty() {
continue;
}
out.push(WebTemplateConstraintBinding {
attr: attr.to_owned(),
ac_code: ac_code.to_owned(),
terminology: set.terminology.clone(),
query_uri: query_uri.to_owned(),
});
}
}
out
}
}
fn constraint_bindings_of(
ctx: &Ctx,
co: &CObject,
arch_id: &str,
) -> Vec<WebTemplateConstraintBinding> {
let mut out = Vec::new();
if let CObject::ConstraintRef(cr) = co {
out.extend(ctx.constraint_bindings_for(arch_id, &cr.reference, ""));
}
for attr in inputs::attributes(co) {
let attr_name = inputs::attribute_name(attr);
for child in inputs::attribute_children(attr) {
if let CObject::ConstraintRef(cr) = child {
out.extend(ctx.constraint_bindings_for(arch_id, &cr.reference, attr_name));
}
}
}
out
}
fn binding_coded_value(item: &TermBindingItem) -> Option<WebTemplateBindingCodedValue> {
let code_phrase = &item.value;
if code_phrase.code_string.trim().is_empty() {
return None;
}
let terminology_id = if code_phrase.terminology_id.value.is_empty() {
item.code.clone()
} else {
code_phrase.terminology_id.value.clone()
};
Some(WebTemplateBindingCodedValue {
value: code_phrase.code_string.clone(),
terminology_id,
})
}
struct ArchetypeLabels<'a> {
ctx: &'a Ctx,
arch_id: &'a str,
}
impl Labels for ArchetypeLabels<'_> {
fn text(&self, terminology: &str, code: &str) -> Option<String> {
self.ctx
.text(self.arch_id, code, &self.ctx.default_language)
.or_else(|| {
self.ctx.text(
self.arch_id,
&format!("{terminology}::{code}"),
&self.ctx.default_language,
)
})
}
fn localized(&self, terminology: &str, code: &str) -> IndexMap<String, String> {
let mut out = IndexMap::new();
for lang in &self.ctx.languages {
if let Some(t) = self.ctx.text(self.arch_id, code, lang).or_else(|| {
self.ctx
.text(self.arch_id, &format!("{terminology}::{code}"), lang)
}) {
out.insert(lang.clone(), t);
}
}
out
}
fn term_bindings(&self, code: &str) -> IndexMap<String, WebTemplateBindingCodedValue> {
self.ctx.term_bindings_for(self.arch_id, code)
}
}
pub fn build_web_template(
opt: &OperationalTemplate,
) -> Result<WebTemplate, crate::flat::error::FlatError> {
let template_id = opt.template_id.value.clone();
if template_id.is_empty() {
return Err(crate::flat::error::FlatError::InvalidTemplate(
"template_id is mandatory".to_owned(),
));
}
let default_language = opt.language.code_string.clone();
let ctx = Ctx {
languages: collect_languages(opt, &default_language),
ontology: collect_ontology(opt, &default_language),
term_bindings: collect_term_bindings(opt),
constraint_bindings: collect_constraint_bindings(opt),
default_language,
};
let root_arch_id = opt.definition.archetype_id.value.clone();
let root_co = CObject::CArchetypeRoot(opt.definition.clone());
let mut tree = build_node(
&ctx,
None,
&root_co,
"",
&root_arch_id,
None,
shape::Identity::Archetyped,
);
tree = shape::compact(tree, 1).unwrap_or_else(shape::tree_placeholder);
shape::synthesize_in_context(&mut tree);
super::id::build_ids(&mut tree);
crate::flat::validation::prepare_walk(&mut tree);
Ok(WebTemplate {
template_id,
sem_ver: None,
version: shape::CURRENT_VERSION.to_owned(),
default_language: ctx.default_language.clone(),
languages: ctx.languages.clone(),
tree,
other_details: other_details(opt),
})
}
fn build_node(
ctx: &Ctx,
owner: Option<&openehr_its::opt14::types::CAttribute>,
co: &CObject,
parent_path: &str,
parent_arch_id: &str,
group: Option<&str>,
identity: shape::Identity,
) -> WebTemplateNode {
let node_arch_id = match co {
CObject::CArchetypeRoot(r) => r.archetype_id.value.as_str(),
_ => parent_arch_id,
};
let mut node = create_node(ctx, owner, co, parent_path, node_arch_id, identity);
build_children(ctx, co, &mut node, node_arch_id, group);
node
}
fn create_node(
ctx: &Ctx,
owner: Option<&openehr_its::opt14::types::CAttribute>,
co: &CObject,
parent_path: &str,
arch_id: &str,
identity: shape::Identity,
) -> WebTemplateNode {
let attr_name = owner.map(inputs::attribute_name);
let archetyped = identity == shape::Identity::Archetyped;
let rm_type = object_rm_type(co).to_owned();
let arch_node_id = if archetyped {
object_archetype_node_id(co)
} else {
String::new()
};
let (occ_min, max) = occurrences(object_occurrences(co));
let min = meet_single_existence(occ_min, owner);
let name_constraint = if archetyped {
name_constraint(co)
} else {
None
};
let path = build_path(
parent_path,
attr_name,
&arch_node_id,
name_constraint.as_deref(),
);
let mut node = WebTemplateNode::new(rm_type, path);
node.node_id = if arch_node_id.is_empty() {
None
} else {
Some(arch_node_id)
};
node.min = min;
node.max = max;
let name_code = if archetyped { object_node_id(co) } else { "" };
let coded_name = archetyped
.then(|| coded_name_constraint(co, ctx, arch_id, name_constraint.as_deref()))
.flatten();
apply_node_naming(
&mut node,
ctx,
arch_id,
name_code,
name_constraint.as_deref(),
coded_name,
);
node.term_bindings = ctx.term_bindings_for(arch_id, name_code);
node.constraint_bindings = constraint_bindings_of(ctx, co, arch_id);
node
}
fn apply_node_naming(
node: &mut WebTemplateNode,
ctx: &Ctx,
arch_id: &str,
name_code: &str,
name_constraint: Option<&str>,
coded_name: Option<(String, CodedName)>,
) {
if let Some((value, coded)) = coded_name {
node.name = Some(value.clone());
node.localized_name = Some(value);
node.name_coded = Some(coded);
} else if let Some(nc) = name_constraint {
node.name = Some(nc.to_owned());
node.localized_name = Some(nc.to_owned());
} else if !name_code.is_empty() {
node.name = ctx.text(arch_id, name_code, &ctx.default_language);
node.localized_name.clone_from(&node.name);
apply_localized_rubrics(node, ctx, arch_id, name_code);
}
node.name_code = if name_code.is_empty() {
None
} else {
Some(name_code.to_owned())
};
}
fn apply_localized_rubrics(node: &mut WebTemplateNode, ctx: &Ctx, arch_id: &str, name_code: &str) {
for lang in &ctx.languages {
let Some(r) = ctx.rubric(arch_id, lang, name_code) else {
continue;
};
if let Some(t) = &r.text {
node.localized_names.insert(lang.clone(), t.clone());
}
if let Some(d) = &r.description {
node.localized_descriptions.insert(lang.clone(), d.clone());
}
}
}
fn build_attribute_children(
ctx: &Ctx,
attr: &openehr_its::opt14::types::CAttribute,
node: &WebTemplateNode,
arch_id: &str,
children: &mut Vec<WebTemplateNode>,
) {
let attr_name = inputs::attribute_name(attr);
let child_group = inputs::openehr_group(&node.rm_type, attr_name);
let merged = attr_name == shape::MERGED_ATTRIBUTE;
let identity = if merged {
shape::Identity::AttributeOnly
} else {
shape::Identity::Archetyped
};
let mut built = Vec::new();
for child_co in inputs::attribute_children(attr) {
if matches!(
child_co,
CObject::ArchetypeSlot(_) | CObject::ConstraintRef(_)
) {
continue;
}
built.push(build_node(
ctx,
Some(attr),
child_co,
&node.aql_path,
arch_id,
child_group,
identity,
));
}
if !merged {
children.extend(built);
return;
}
if let Some(mut transition) = shape::merge_alternatives(built) {
let (min, max) = occurrences(attribute_existence(attr));
transition.min = min;
transition.max = max;
children.push(transition);
}
}
fn build_children(
ctx: &Ctx,
co: &CObject,
node: &mut WebTemplateNode,
arch_id: &str,
group: Option<&str>,
) {
let is_data_value = node.rm_type.starts_with("DV_");
let recurse_attrs = !is_data_value || node.rm_type.starts_with("DV_INTERVAL");
let mut children = Vec::new();
if recurse_attrs {
for attr in inputs::attributes(co) {
if inputs::attribute_name(attr) != "name" {
build_attribute_children(ctx, attr, node, arch_id, &mut children);
}
}
}
if has_inputs(&node.rm_type) && (children.is_empty() || is_party(&node.rm_type)) {
let labels = ArchetypeLabels { ctx, arch_id };
let (built, ptypes) = inputs::build_inputs(&node.rm_type, co, &labels, group);
node.inputs = built;
node.proportion_types = ptypes;
capture_leaf_constraints(co, node);
}
node.cardinalities = cardinalities(co, &node.aql_path);
node.card_all = all_cardinalities(co, &node.aql_path);
if recurse_attrs {
node.existence = existence_constraints(co, &node.aql_path);
node.closed_attributes = closed_attributes(co, &node.aql_path);
if shape::is_entry_family(&node.rm_type) {
node.structural_stubs = structural_stubs(ctx, co, arch_id);
}
}
node.children = children;
shape::post_process(node);
}
fn has_inputs(rm_type: &str) -> bool {
rm_type.starts_with("DV_") || is_party(rm_type) || rm_type == "CODE_PHRASE"
}
fn is_party(rm_type: &str) -> bool {
matches!(
rm_type,
"PARTY_PROXY" | "PARTY_IDENTIFIED" | "PARTY_RELATED"
)
}
fn cardinalities(co: &CObject, node_path: &str) -> Vec<WebTemplateCardinality> {
let mut out = Vec::new();
for attr in inputs::attributes(co) {
if let openehr_its::opt14::types::CAttribute::CMultipleAttribute(m) = attr
&& requires_cardinality(&m.cardinality, m.children.len())
{
let (min, max) = occurrences(&m.cardinality.interval);
out.push(WebTemplateCardinality {
min,
max,
ids: None,
path: format!("{node_path}/{}", m.rm_attribute_name),
});
}
}
out
}
fn all_cardinalities(co: &CObject, node_path: &str) -> Vec<WebTemplateCardinality> {
let mut out = Vec::new();
for attr in inputs::attributes(co) {
if let openehr_its::opt14::types::CAttribute::CMultipleAttribute(m) = attr {
let (min, max) = occurrences(&m.cardinality.interval);
if min.unwrap_or(0) >= 1 || max != -1 {
out.push(WebTemplateCardinality {
min,
max,
ids: None,
path: format!("{node_path}/{}", m.rm_attribute_name),
});
}
}
}
out
}
fn capture_leaf_constraints(co: &CObject, node: &mut WebTemplateNode) {
for datum in ["magnitude", "value", "numerator", "denominator", "size"] {
match inputs::primitive_under(co, datum) {
Some(CPrimitive::CInteger(ci)) if !ci.list.is_empty() => {
node.numeric_lists.push((
datum.to_owned(),
ci.list.iter().map(|v| f64::from(*v)).collect(),
));
}
Some(CPrimitive::CReal(cr)) if !cr.list.is_empty() => {
node.numeric_lists.push((datum.to_owned(), cr.list.clone()));
}
_ => {}
}
}
capture_size_range(co, node);
capture_leaf_existence(co, node);
capture_duration_range(co, node);
node.tz_validity = match inputs::primitive_under(co, "value") {
Some(CPrimitive::CTime(c)) => c.timezone_validity.map(validity_code),
Some(CPrimitive::CDateTime(c)) => c.timezone_validity.map(validity_code),
_ => None,
};
capture_quantity_property(co, node);
capture_code_lists(co, node);
}
fn capture_duration_range(co: &CObject, node: &mut WebTemplateNode) {
let Some(CPrimitive::CDuration(d)) = inputs::primitive_under(co, "value") else {
return;
};
let Some(range) = &d.range else { return };
let min = if range.lower_unbounded {
None
} else {
range.lower.clone()
};
let max = if range.upper_unbounded {
None
} else {
range.upper.clone()
};
if min.is_none() && max.is_none() {
return;
}
let min_strict = range.lower_included == Some(false);
let max_strict = range.upper_included == Some(false);
node.duration_range = Some(super::model::WebTemplateRange {
min_op: min
.as_ref()
.map(|_| if min_strict { ">" } else { ">=" }.to_owned()),
min: min.map(serde_json::Value::String),
max_op: max
.as_ref()
.map(|_| if max_strict { "<" } else { "<=" }.to_owned()),
max: max.map(serde_json::Value::String),
});
}
fn capture_quantity_property(co: &CObject, node: &mut WebTemplateNode) {
if let CObject::CDvQuantity(q) = co
&& let Some(property) = &q.property
&& property
.terminology_id
.value
.eq_ignore_ascii_case("openehr")
&& !property.code_string.is_empty()
&& property.code_string != "0"
{
node.quantity_property = Some(property.code_string.clone());
}
}
fn capture_code_lists(co: &CObject, node: &mut WebTemplateNode) {
let defining_cp = match co {
CObject::CCodePhrase(cp) => Some(cp),
_ => inputs::attr_children(co, "defining_code").find_map(|c| match c {
CObject::CCodePhrase(cp) => Some(cp),
_ => None,
}),
};
if let Some(cp) = defining_cp
&& !cp.code_list.is_empty()
&& cp
.terminology_id
.as_ref()
.is_some_and(|t| t.value.eq_ignore_ascii_case("local"))
{
node.coded_terminology_local = true;
}
for attr in inputs::attributes(co) {
let attr_name = inputs::attribute_name(attr);
if attr_name == "defining_code" {
continue; }
for child in inputs::attribute_children(attr) {
if let CObject::CCodePhrase(cp) = child
&& !cp.code_list.is_empty()
{
node.code_lists.push(WebTemplateCodeList {
attr: attr_name.to_owned(),
terminology: cp
.terminology_id
.as_ref()
.map(|t| t.value.clone())
.filter(|t| !t.is_empty()),
codes: cp.code_list.clone(),
});
}
}
}
}
fn capture_size_range(co: &CObject, node: &mut WebTemplateNode) {
let Some(CPrimitive::CInteger(ci)) = inputs::primitive_under(co, "size") else {
return;
};
let Some(iv) = &ci.range else { return };
let min = if iv.lower_unbounded { None } else { iv.lower };
let max = if iv.upper_unbounded { None } else { iv.upper };
if min.is_none() && max.is_none() {
return;
}
let min_strict = iv.lower_included == Some(false);
let max_strict = iv.upper_included == Some(false);
node.numeric_ranges.push((
"size".to_owned(),
super::model::WebTemplateRange {
min_op: min.map(|_| if min_strict { ">" } else { ">=" }.to_owned()),
min: min.map(serde_json::Value::from),
max_op: max.map(|_| if max_strict { "<" } else { "<=" }.to_owned()),
max: max.map(serde_json::Value::from),
},
));
}
fn capture_leaf_existence(co: &CObject, node: &mut WebTemplateNode) {
for attr in inputs::attributes(co) {
let openehr_its::opt14::types::CAttribute::CSingleAttribute(s) = attr else {
continue;
};
if s.rm_attribute_name == "name" {
continue;
}
let constrained = node.inputs.iter().any(|i| {
i.suffix.as_deref() == Some(s.rm_attribute_name.as_str())
&& ((!i.list.is_empty() && i.list_open != Some(true))
|| i.validation.as_ref().is_some_and(|v| v.pattern.is_some()))
});
if !constrained {
continue;
}
let (min, max) = occurrences(&s.existence);
if min.unwrap_or(0) >= 1 {
node.existence.push(WebTemplateExistence {
min: min.unwrap_or(0),
max,
path: format!("{}/{}", node.aql_path, s.rm_attribute_name),
});
}
}
}
fn existence_constraints(co: &CObject, node_path: &str) -> Vec<WebTemplateExistence> {
let mut out = Vec::new();
for attr in inputs::attributes(co) {
if let openehr_its::opt14::types::CAttribute::CMultipleAttribute(m) = attr {
let (min, max) = occurrences(&m.existence);
if min.unwrap_or(0) >= 1 {
out.push(WebTemplateExistence {
min: min.unwrap_or(0),
max,
path: format!("{node_path}/{}", m.rm_attribute_name),
});
}
continue;
}
let openehr_its::opt14::types::CAttribute::CSingleAttribute(s) = attr else {
continue;
};
let (min, max) = occurrences(&s.existence);
let min = min.unwrap_or(0);
let object_valued = s.children.is_empty()
|| s.children
.iter()
.any(|c| !matches!(c, CObject::CPrimitiveObject(_)));
if min >= 1 && s.rm_attribute_name != "name" && object_valued {
out.push(WebTemplateExistence {
min,
max,
path: format!("{node_path}/{}", s.rm_attribute_name),
});
}
}
out
}
fn closed_attributes(co: &CObject, node_path: &str) -> Vec<WebTemplateClosedAttribute> {
inputs::attributes(co)
.iter()
.filter_map(|attr| closed_attribute(attr, node_path))
.collect()
}
fn closed_attribute(
attr: &openehr_its::opt14::types::CAttribute,
node_path: &str,
) -> Option<WebTemplateClosedAttribute> {
let attr_name = inputs::attribute_name(attr);
if attr_name == "name" {
return None;
}
if inputs::attribute_children(attr).iter().any(|c| {
matches!(
c,
CObject::ArchetypeInternalRef(_) | CObject::ConstraintRef(_)
)
}) {
return None;
}
let (allowed_ids, slots) = admissible_children(attr);
if allowed_ids.is_empty() && slots.is_empty() {
return None;
}
Some(WebTemplateClosedAttribute {
path: format!("{node_path}/{attr_name}"),
allowed_ids,
slots,
})
}
fn admissible_children(
attr: &openehr_its::opt14::types::CAttribute,
) -> (Vec<String>, Vec<WebTemplateArchetypeSlot>) {
let mut allowed_ids: Vec<String> = Vec::new();
let mut slots: Vec<WebTemplateArchetypeSlot> = Vec::new();
for child in inputs::attribute_children(attr) {
if let CObject::ArchetypeSlot(s) = child {
slots.push(archetype_slot(s));
continue;
}
let id = object_archetype_node_id(child);
if !id.is_empty() {
allowed_ids.push(id);
}
}
(allowed_ids, slots)
}
const ENTRY_STRUCTURAL_ATTRS: [&str; 4] = ["data", "description", "protocol", "state"];
fn structural_stubs(ctx: &Ctx, co: &CObject, arch_id: &str) -> Vec<WebTemplateStructuralStub> {
let mut out = Vec::new();
for attr in inputs::attributes(co) {
let attr_name = inputs::attribute_name(attr);
if !ENTRY_STRUCTURAL_ATTRS.contains(&attr_name) {
continue;
}
for child in inputs::attribute_children(attr) {
if matches!(
child,
CObject::ArchetypeSlot(_)
| CObject::ConstraintRef(_)
| CObject::ArchetypeInternalRef(_)
) {
continue;
}
let node_id = object_archetype_node_id(child);
if node_id.is_empty() {
continue;
}
out.push(WebTemplateStructuralStub {
attr: attr_name.to_owned(),
rm_type: object_rm_type(child).to_owned(),
node_id,
name: ctx.text(arch_id, object_node_id(child), &ctx.default_language),
});
break; }
}
out
}
fn archetype_slot(s: &openehr_its::opt14::types::ArchetypeSlot) -> WebTemplateArchetypeSlot {
let (min, max) = occurrences(&s.occurrences);
WebTemplateArchetypeSlot {
rm_type: s.rm_type_name.clone(),
min: min.unwrap_or(0).max(0),
max,
includes: s.includes.iter().filter_map(slot_pattern).collect(),
excludes: s.excludes.iter().filter_map(slot_pattern).collect(),
}
}
fn slot_pattern(a: &Assertion) -> Option<String> {
expression_pattern(a).or_else(|| string_expression_pattern(a))
}
fn validity_code(kind: ValidityKind) -> i32 {
match kind {
ValidityKind::Mandatory => 1001,
ValidityKind::Optional => 1002,
ValidityKind::Disallowed => 1003,
}
}
fn expression_pattern(a: &Assertion) -> Option<String> {
let ExprItem::ExprBinaryOperator(op) = a.expression.as_ref() else {
return None;
};
if op.operator != OperatorKind::Matches {
return None;
}
let (ExprItem::ExprLeaf(left), ExprItem::ExprLeaf(right)) =
(op.left_operand.as_ref(), op.right_operand.as_ref())
else {
return None;
};
if !is_archetype_id_reference(&left.item.text()) {
return None;
}
let pattern = c_string_constraint(&right.item)?.child("pattern")?.text();
let pattern = pattern.trim();
(!pattern.is_empty()).then(|| pattern.to_owned())
}
fn string_expression_pattern(a: &Assertion) -> Option<String> {
let s = a.string_expression.as_deref()?;
if !is_archetype_id_reference(s) {
return None;
}
let (_, rest) = s.split_once("matches {/")?;
let end = rest.rfind("/}")?;
Some(rest.get(..end)?.to_owned())
}
fn is_archetype_id_reference(operand: &str) -> bool {
let operand = operand.trim();
operand == "archetype_id"
|| operand
.strip_prefix("archetype_id")
.is_some_and(|rest| rest.starts_with('/'))
}
fn c_string_constraint(
item: &openehr_its::xml::runtime::XmlAny,
) -> Option<&openehr_its::xml::runtime::XmlAny> {
match item.xsi_type()? {
"C_STRING" => Some(item),
"C_PRIMITIVE_OBJECT" => item
.child("item")
.filter(|inner| inner.xsi_type() == Some("C_STRING")),
_ => None,
}
}
fn requires_cardinality(card: &Cardinality, children_count: usize) -> bool {
let iv = &card.interval;
let min = if iv.lower_unbounded { None } else { iv.lower };
let max = if iv.upper_unbounded { None } else { iv.upper };
if min.is_none() && max.is_none() {
return false;
}
let Some(min) = min else { return false };
if min == 0 {
return false;
}
let count = i32::try_from(children_count).unwrap_or(i32::MAX);
if min == 1 && max == Some(1) && children_count == 1 {
false
} else {
min > 1 || max.is_some_and(|m| m < count)
}
}
fn collect_languages(opt: &OperationalTemplate, default_language: &str) -> Vec<String> {
let mut langs = vec![default_language.to_owned()];
let mut push = |flat: &openehr_its::opt14::types::FlatArchetypeOntology| {
for set in &flat.term_definitions {
if !langs.contains(&set.language) {
langs.push(set.language.clone());
}
}
};
if let Some(o) = &opt.ontology {
push(o);
}
for o in &opt.component_ontologies {
push(o);
}
langs
}
fn collect_ontology(opt: &OperationalTemplate, default_language: &str) -> Ontology {
let mut ontology: Ontology = HashMap::new();
let mut register_flat = |flat: &openehr_its::opt14::types::FlatArchetypeOntology| {
let arch = ontology.entry(flat.archetype_id.clone()).or_default();
for set in &flat.term_definitions {
let lang = arch.entry(set.language.clone()).or_default();
for term in &set.items {
lang.entry(term.code.clone())
.or_insert_with(|| rubric_of(term));
}
}
};
if let Some(o) = &opt.ontology {
register_flat(o);
}
for o in &opt.component_ontologies {
register_flat(o);
}
collect_inline_terms(&opt.definition, default_language, &mut ontology);
ontology
}
fn collect_inline_terms(root: &CArchetypeRoot, default_language: &str, ontology: &mut Ontology) {
let arch = ontology.entry(root.archetype_id.value.clone()).or_default();
let lang = arch.entry(default_language.to_owned()).or_default();
for term in &root.term_definitions {
lang.entry(term.code.clone())
.or_insert_with(|| rubric_of(term));
}
let mut nested = Vec::new();
for attr in &root.attributes {
for child in inputs::attribute_children(attr) {
collect_nested_roots(child, &mut nested);
}
}
for r in nested {
collect_inline_terms(r, default_language, ontology);
}
}
fn collect_nested_roots<'a>(co: &'a CObject, out: &mut Vec<&'a CArchetypeRoot>) {
if let CObject::CArchetypeRoot(r) = co {
out.push(r);
return;
}
for attr in inputs::attributes(co) {
for child in inputs::attribute_children(attr) {
collect_nested_roots(child, out);
}
}
}
fn collect_term_bindings(opt: &OperationalTemplate) -> TermBindings {
let mut out: TermBindings = HashMap::new();
collect_root_bindings(&opt.definition, &mut out);
out
}
fn collect_root_bindings(root: &CArchetypeRoot, out: &mut TermBindings) {
if !root.term_bindings.is_empty() {
out.entry(root.archetype_id.value.clone())
.or_insert_with(|| root.term_bindings.clone());
}
let mut nested = Vec::new();
for attr in &root.attributes {
for child in inputs::attribute_children(attr) {
collect_nested_roots(child, &mut nested);
}
}
for r in nested {
collect_root_bindings(r, out);
}
}
fn collect_constraint_bindings(opt: &OperationalTemplate) -> ConstraintBindings {
let mut out: ConstraintBindings = HashMap::new();
for onto in opt.ontology.iter().chain(opt.component_ontologies.iter()) {
if onto.constraint_bindings.is_empty() {
continue;
}
out.entry(onto.archetype_id.clone())
.or_default()
.extend(onto.constraint_bindings.iter().cloned());
}
out
}
fn rubric_of(term: &ArchetypeTerm) -> Rubric {
Rubric {
text: term.items.get("text").cloned(),
description: term.items.get("description").cloned(),
}
}
fn other_details(opt: &OperationalTemplate) -> IndexMap<String, String> {
let mut out = IndexMap::new();
if let Some(desc) = &opt.description
&& let Some(details) = &desc.other_details
{
if let Some(v) = details.get("is_singleton") {
out.insert("is_singleton".to_owned(), v.clone());
}
}
out
}
fn object_rm_type(co: &CObject) -> &str {
match co {
CObject::CComplexObject(c) => &c.rm_type_name,
CObject::CArchetypeRoot(c) => &c.rm_type_name,
CObject::CCodePhrase(c) => &c.rm_type_name,
CObject::CCodeReference(c) => &c.rm_type_name,
CObject::CDvOrdinal(c) => &c.rm_type_name,
CObject::CDvQuantity(c) => &c.rm_type_name,
CObject::CDvState(c) => &c.rm_type_name,
CObject::CPrimitiveObject(c) => &c.rm_type_name,
CObject::CDefinedObject(c) => &c.rm_type_name,
CObject::ArchetypeInternalRef(c) => &c.rm_type_name,
CObject::ArchetypeSlot(c) => &c.rm_type_name,
CObject::ConstraintRef(c) => &c.rm_type_name,
CObject::TComplexObject(c) => &c.rm_type_name,
}
}
fn object_node_id(co: &CObject) -> &str {
match co {
CObject::CComplexObject(c) => &c.node_id,
CObject::CArchetypeRoot(c) => &c.node_id,
CObject::CCodePhrase(c) => &c.node_id,
CObject::CCodeReference(c) => &c.node_id,
CObject::CDvOrdinal(c) => &c.node_id,
CObject::CDvQuantity(c) => &c.node_id,
CObject::CDvState(c) => &c.node_id,
CObject::CPrimitiveObject(c) => &c.node_id,
CObject::CDefinedObject(c) => &c.node_id,
CObject::ArchetypeInternalRef(c) => &c.node_id,
CObject::ArchetypeSlot(c) => &c.node_id,
CObject::ConstraintRef(c) => &c.node_id,
CObject::TComplexObject(c) => &c.node_id,
}
}
fn object_archetype_node_id(co: &CObject) -> String {
match co {
CObject::CArchetypeRoot(c) => c.archetype_id.value.clone(),
_ => object_node_id(co).to_owned(),
}
}
fn object_occurrences(co: &CObject) -> &Intervalofinteger {
match co {
CObject::CComplexObject(c) => &c.occurrences,
CObject::CArchetypeRoot(c) => &c.occurrences,
CObject::CCodePhrase(c) => &c.occurrences,
CObject::CCodeReference(c) => &c.occurrences,
CObject::CDvOrdinal(c) => &c.occurrences,
CObject::CDvQuantity(c) => &c.occurrences,
CObject::CDvState(c) => &c.occurrences,
CObject::CPrimitiveObject(c) => &c.occurrences,
CObject::CDefinedObject(c) => &c.occurrences,
CObject::ArchetypeInternalRef(c) => &c.occurrences,
CObject::ArchetypeSlot(c) => &c.occurrences,
CObject::ConstraintRef(c) => &c.occurrences,
CObject::TComplexObject(c) => &c.occurrences,
}
}
fn attribute_existence(attr: &openehr_its::opt14::types::CAttribute) -> &Intervalofinteger {
match attr {
openehr_its::opt14::types::CAttribute::CSingleAttribute(s) => &s.existence,
openehr_its::opt14::types::CAttribute::CMultipleAttribute(m) => &m.existence,
}
}
fn meet_single_existence(
occurrences_min: Option<i32>,
owner: Option<&openehr_its::opt14::types::CAttribute>,
) -> Option<i32> {
let Some(openehr_its::opt14::types::CAttribute::CSingleAttribute(s)) = owner else {
return occurrences_min;
};
let existence_min = occurrences(&s.existence).0.unwrap_or(0);
occurrences_min.map(|m| m.min(existence_min))
}
fn occurrences(iv: &Intervalofinteger) -> (Option<i32>, i32) {
let min = if iv.lower_unbounded { None } else { iv.lower };
let max = if iv.upper_unbounded { None } else { iv.upper };
(min, max.unwrap_or(-1))
}
fn name_constraint(co: &CObject) -> Option<String> {
for name_child in inputs::attr_children(co, "name") {
if let Some(CPrimitive::CString(cs)) = inputs::primitive_under(name_child, "value")
&& cs.list.len() == 1
{
return cs.list.first().cloned();
}
}
None
}
fn coded_name_constraint(
co: &CObject,
ctx: &Ctx,
arch_id: &str,
explicit: Option<&str>,
) -> Option<(String, CodedName)> {
let name_child =
inputs::attr_children(co, "name").find(|c| object_rm_type(c) == "DV_CODED_TEXT")?;
let code_phrase = inputs::attr_children(name_child, "defining_code").find_map(|c| match c {
CObject::CCodePhrase(cp) => Some(cp),
_ => None,
})?;
let (first, rest) = code_phrase.code_list.split_first()?;
let terminology = code_phrase
.terminology_id
.as_ref()
.map(|t| t.value.clone())
.filter(|t| !t.is_empty())
.unwrap_or_else(|| "local".to_owned());
let matched = explicit.and_then(|v| {
std::iter::once(first)
.chain(rest)
.find(|c| ctx.text(arch_id, c, &ctx.default_language).as_deref() == Some(v))
.cloned()
});
let incoherent = explicit.is_some() && matched.is_none() && !rest.is_empty();
let code = matched.unwrap_or_else(|| first.clone());
let value = explicit
.map(str::to_owned)
.or_else(|| ctx.text(arch_id, &code, &ctx.default_language))
.unwrap_or_else(|| code.clone());
Some((
value,
CodedName {
terminology,
code,
incoherent,
},
))
}
fn build_path(
parent_path: &str,
attr_name: Option<&str>,
arch_node_id: &str,
name_constraint: Option<&str>,
) -> String {
let Some(attr) = attr_name else {
return String::new(); };
let predicate = if arch_node_id.is_empty() {
String::new()
} else if let Some(name) = name_constraint {
format!("[{arch_node_id},'{name}']")
} else {
format!("[{arch_node_id}]")
};
format!("{parent_path}/{attr}{predicate}")
}