use indexmap::IndexMap;
use super::model::{
WebTemplateCardinality, WebTemplateClosedAttribute, WebTemplateExistence, WebTemplateInput,
WebTemplateInputType, WebTemplateNode, WebTemplateSlot, WebTemplateStructuralStub,
};
pub(super) const CURRENT_VERSION: &str = "2.3";
const ALWAYS_COMPACTABLE: [&str; 6] = [
"ITEM_TREE",
"ITEM_LIST",
"ITEM_SINGLE",
"ITEM_TABLE",
"ITEM_STRUCTURE",
"HISTORY",
];
const SINGLE_COMPACTABLE: [&str; 3] = ["POINT_EVENT", "INTERVAL_EVENT", "EVENT"];
const SKIP_IF_EMPTY: [&str; 12] = [
"CLUSTER",
"ELEMENT",
"ITEM_TREE",
"ITEM_LIST",
"ITEM_SINGLE",
"ITEM_TABLE",
"ITEM_STRUCTURE",
"HISTORY",
"POINT_EVENT",
"INTERVAL_EVENT",
"EVENT",
"ITEM",
];
pub(super) fn tree_placeholder() -> WebTemplateNode {
WebTemplateNode::new("COMPOSITION".to_owned(), String::new())
}
fn is_entry_family(rm_type: &str) -> bool {
matches!(
rm_type,
"OBSERVATION" | "EVALUATION" | "INSTRUCTION" | "ACTION" | "ADMIN_ENTRY" | "GENERIC_ENTRY"
)
}
fn is_event_family(rm_type: &str) -> bool {
matches!(rm_type, "EVENT" | "POINT_EVENT" | "INTERVAL_EVENT")
}
pub(super) fn post_process(node: &mut WebTemplateNode) {
match node.rm_type.as_str() {
"ELEMENT" => post_process_element(node),
"COMPOSITION" => post_process_composition(node),
"OBSERVATION" => post_process_observation(node),
_ => {}
}
}
fn post_process_element(node: &mut WebTemplateNode) {
if let [first_child, second_child] = node.children.as_slice()
&& first_child.aql_path == second_child.aql_path
{
let first = first_child.first_input_type();
let second = second_child.first_input_type();
if first == Some(WebTemplateInputType::CodedText)
&& second == Some(WebTemplateInputType::Text)
{
compact_to_coded_with_other(node, 0, 1);
} else if second == Some(WebTemplateInputType::CodedText)
&& first == Some(WebTemplateInputType::Text)
{
compact_to_coded_with_other(node, 1, 0);
}
}
if node.children.len() > 1 {
for child in &mut node.children {
child.min = Some(0);
}
}
}
fn compact_to_coded_with_other(node: &mut WebTemplateNode, coded: usize, text: usize) {
let Some(coded_child) = node.children.get_mut(coded) else {
return;
};
if let Some(input) = coded_child.inputs.first_mut() {
input.list_open = Some(true);
}
let mut other = WebTemplateInput::new(WebTemplateInputType::Text, Some("other"));
other.list_open = Some(true);
coded_child.inputs.push(other);
node.children.remove(text);
}
fn post_process_composition(node: &mut WebTemplateNode) {
let mut category = None;
let mut language = None;
for (i, child) in node.children.iter().enumerate() {
if child.aql_path == "/category" {
category = Some(i);
} else if child.aql_path == "/language" {
language = Some(i);
}
}
if let Some(cat) = category {
let node_cat = node.children.remove(cat);
match language {
Some(lang) if lang > 0 => node.children.insert(lang - 1, node_cat),
_ => node.children.push(node_cat),
}
}
}
fn post_process_observation(node: &mut WebTemplateNode) {
let data_path = format!("{}/data", node.aql_path);
let state_prefix = format!("{}/state", node.aql_path);
let protocol_prefix = format!("{}/protocol", node.aql_path);
for child in &mut node.children {
if child.aql_path.starts_with(&state_prefix) || child.aql_path.starts_with(&protocol_prefix)
{
child.depends_on = Some(vec![data_path.clone()]);
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum Identity {
Archetyped,
AttributeOnly,
}
pub(super) const MERGED_ATTRIBUTE: &str = "ism_transition";
pub(super) fn merge_alternatives(nodes: Vec<WebTemplateNode>) -> Option<WebTemplateNode> {
nodes.into_iter().reduce(|mut acc, other| {
merge_into(&mut acc, other);
acc
})
}
fn merge_into(acc: &mut WebTemplateNode, other: WebTemplateNode) {
acc.min = match (acc.min, other.min) {
(Some(a), Some(b)) => Some(a.min(b)),
_ => None,
};
acc.max = if acc.max == -1 || other.max == -1 {
-1
} else {
acc.max.max(other.max)
};
acc.coded_terminology_local &= other.coded_terminology_local;
merge_inputs(&mut acc.inputs, other.inputs);
merge_children(acc, other.children);
merge_existence(&mut acc.existence, other.existence);
extend_by_key(&mut acc.cardinalities, other.cardinalities, |c| {
c.path.clone()
});
extend_by_key(&mut acc.card_all, other.card_all, |c| c.path.clone());
extend_by_key(&mut acc.slots, other.slots, |s| {
(s.path.clone(), s.rm_type.clone())
});
extend_by_key(&mut acc.closed_attributes, other.closed_attributes, |c| {
c.path.clone()
});
extend_by_key(&mut acc.code_lists, other.code_lists, |c| {
(c.attr.clone(), c.terminology.clone())
});
extend_by_key(
&mut acc.constraint_bindings,
other.constraint_bindings,
|b| (b.attr.clone(), b.ac_code.clone(), b.terminology.clone()),
);
for (terminology, binding) in other.term_bindings {
acc.term_bindings.entry(terminology).or_insert(binding);
}
}
fn merge_children(acc: &mut WebTemplateNode, children: Vec<WebTemplateNode>) {
let mut matched = vec![false; acc.children.len()];
for child in children {
let hit = acc
.children
.iter()
.position(|c| c.aql_path == child.aql_path && c.rm_type == child.rm_type);
if let Some(index) = hit {
if let Some(seen) = matched.get_mut(index) {
*seen = true;
}
if let Some(existing) = acc.children.get_mut(index) {
merge_into(existing, child);
}
} else {
let mut child = child;
child.min = Some(0);
acc.children.push(child);
}
}
for (existing, seen) in acc.children.iter_mut().zip(&matched) {
if !*seen {
existing.min = Some(0);
}
}
}
fn merge_inputs(acc: &mut Vec<WebTemplateInput>, inputs: Vec<WebTemplateInput>) {
for input in inputs {
let hit = acc
.iter()
.position(|i| i.suffix == input.suffix && i.input_type == input.input_type);
if let Some(existing) = hit.and_then(|index| acc.get_mut(index)) {
for option in input.list {
if !existing.list.iter().any(|held| held.value == option.value) {
existing.list.push(option);
}
}
} else {
acc.push(input);
}
}
}
fn merge_existence(acc: &mut Vec<WebTemplateExistence>, existence: Vec<WebTemplateExistence>) {
for entry in existence {
let hit = acc.iter().position(|e| e.path == entry.path);
if let Some(held) = hit.and_then(|index| acc.get_mut(index)) {
held.min = held.min.min(entry.min);
} else {
acc.push(entry);
}
}
}
fn extend_by_key<T, K: PartialEq>(acc: &mut Vec<T>, extra: Vec<T>, key: impl Fn(&T) -> K) {
for item in extra {
if !acc.iter().any(|held| key(held) == key(&item)) {
acc.push(item);
}
}
}
pub(super) fn synthesize_in_context(node: &mut WebTemplateNode) {
for child in &mut node.children {
synthesize_in_context(child);
}
if node.rm_type == "COMPOSITION" {
synth_composition(node);
} else if is_entry_family(&node.rm_type) {
synth_entry(node);
} else if is_event_family(&node.rm_type) {
synth_event(node);
}
}
fn synth_composition(node: &mut WebTemplateNode) {
ensure_context(node);
ensure_child(
node,
"/category",
"DV_CODED_TEXT",
None,
category_inputs(),
true,
);
ensure_child(
node,
"/language",
"CODE_PHRASE",
Some("Language"),
vec![],
false,
);
ensure_child(
node,
"/territory",
"CODE_PHRASE",
Some("Territory"),
vec![],
false,
);
ensure_child(
node,
"/composer",
"PARTY_PROXY",
Some("Composer"),
party_inputs(),
false,
);
}
fn ensure_context(node: &mut WebTemplateNode) {
let idx = if let Some(i) = node.children.iter().position(|c| c.aql_path == "/context") {
i
} else {
let mut ctx = WebTemplateNode::new("EVENT_CONTEXT".to_owned(), "/context".to_owned());
ctx.node_id = Some(String::new());
ctx.min = Some(1);
ctx.max = 1;
node.children.insert(0, ctx);
0
};
let Some(ctx) = node.children.get_mut(idx) else {
return;
};
ensure_child(
ctx,
"/context/start_time",
"DV_DATE_TIME",
Some("Start_time"),
datetime_inputs(),
false,
);
ensure_child(
ctx,
"/context/setting",
"DV_CODED_TEXT",
Some("Setting"),
setting_inputs(),
false,
);
}
fn synth_entry(node: &mut WebTemplateNode) {
let base = node.aql_path.clone();
ensure_child(
node,
&format!("{base}/language"),
"CODE_PHRASE",
Some("Language"),
vec![],
false,
);
ensure_child(
node,
&format!("{base}/encoding"),
"CODE_PHRASE",
Some("Encoding"),
vec![],
false,
);
ensure_child(
node,
&format!("{base}/subject"),
"PARTY_PROXY",
Some("Subject"),
party_inputs(),
false,
);
}
fn synth_event(node: &mut WebTemplateNode) {
let base = node.aql_path.clone();
ensure_child(
node,
&format!("{base}/time"),
"DV_DATE_TIME",
Some("Time"),
datetime_inputs(),
false,
);
}
fn ensure_child(
parent: &mut WebTemplateNode,
aql_path: &str,
rm_type: &str,
name: Option<&str>,
inputs: Vec<WebTemplateInput>,
empty_node_id: bool,
) {
if parent.children.iter().any(|c| c.aql_path == aql_path) {
return;
}
let mut n = WebTemplateNode::new(rm_type.to_owned(), aql_path.to_owned());
n.min = Some(1);
n.max = 1;
n.in_context = Some(true);
if let Some(name) = name {
n.name = Some(name.to_owned());
n.localized_name = Some(name.to_owned());
}
if empty_node_id {
n.node_id = Some(String::new());
}
n.inputs = inputs;
parent.children.push(n);
}
fn datetime_inputs() -> Vec<WebTemplateInput> {
vec![WebTemplateInput::new(WebTemplateInputType::Datetime, None)]
}
fn setting_inputs() -> Vec<WebTemplateInput> {
vec![
WebTemplateInput::new(WebTemplateInputType::Text, Some("code")),
WebTemplateInput::new(WebTemplateInputType::Text, Some("value")),
]
}
fn party_inputs() -> Vec<WebTemplateInput> {
["id", "id_scheme", "id_namespace", "name"]
.into_iter()
.map(|s| WebTemplateInput::new(WebTemplateInputType::Text, Some(s)))
.collect()
}
fn category_inputs() -> Vec<WebTemplateInput> {
let mut cv = super::model::WebTemplateCodedValue::new("433", Some("event".to_owned()));
cv.localized_labels
.insert("en".to_owned(), "event".to_owned());
let mut input = WebTemplateInput::new(WebTemplateInputType::CodedText, Some("code"));
input.list = vec![cv];
input.terminology = Some("openehr".to_owned());
vec![input]
}
pub(super) fn compact(mut node: WebTemplateNode, depth: usize) -> Option<WebTemplateNode> {
let children = std::mem::take(&mut node.children);
let mut hoisted = Hoisted {
cardinalities: std::mem::take(&mut node.cardinalities),
existence: std::mem::take(&mut node.existence),
card_all: std::mem::take(&mut node.card_all),
slots: std::mem::take(&mut node.slots),
closed_attributes: std::mem::take(&mut node.closed_attributes),
structural_stubs: std::mem::take(&mut node.structural_stubs),
};
node.children = get_compacted(children, &mut hoisted);
node.cardinalities = hoisted.cardinalities;
node.existence = hoisted.existence;
node.card_all = hoisted.card_all;
node.slots = hoisted.slots;
node.closed_attributes = hoisted.closed_attributes;
node.structural_stubs = hoisted.structural_stubs;
let children = std::mem::take(&mut node.children);
node.children = children
.into_iter()
.filter_map(|c| compact(c, depth + 1))
.collect();
process_children(node, depth)
}
struct Hoisted {
cardinalities: Vec<WebTemplateCardinality>,
existence: Vec<WebTemplateExistence>,
card_all: Vec<WebTemplateCardinality>,
slots: Vec<WebTemplateSlot>,
closed_attributes: Vec<WebTemplateClosedAttribute>,
structural_stubs: Vec<WebTemplateStructuralStub>,
}
fn get_compacted(children: Vec<WebTemplateNode>, parent: &mut Hoisted) -> Vec<WebTemplateNode> {
let originals: Vec<(String, i32)> = children
.iter()
.map(|c| (c.rm_type.clone(), c.max))
.collect();
let mut out = Vec::new();
for mut child in children {
if is_compactable(&child, &originals) {
parent
.cardinalities
.append(&mut child.cardinalities.clone());
parent
.existence
.append(&mut std::mem::take(&mut child.existence));
parent
.card_all
.append(&mut std::mem::take(&mut child.card_all));
parent.slots.append(&mut std::mem::take(&mut child.slots));
parent
.closed_attributes
.append(&mut std::mem::take(&mut child.closed_attributes));
parent
.structural_stubs
.append(&mut std::mem::take(&mut child.structural_stubs));
parent.slots.push(WebTemplateSlot {
path: child.aql_path.clone(),
rm_type: child.rm_type.clone(),
name: child.name.clone(),
});
out.extend(get_compacted(child.children, parent));
} else {
out.push(child);
}
}
out
}
fn is_compactable(child: &WebTemplateNode, siblings: &[(String, i32)]) -> bool {
if ALWAYS_COMPACTABLE.contains(&child.rm_type.as_str()) {
return true;
}
child.max == 1
&& SINGLE_COMPACTABLE.contains(&child.rm_type.as_str())
&& !siblings
.iter()
.any(|(rm, _)| rm != &child.rm_type && types_match(&child.rm_type, rm))
}
fn types_match(a: &str, b: &str) -> bool {
a == b || (a.ends_with("EVENT") && b.ends_with("EVENT"))
}
fn process_children(mut node: WebTemplateNode, depth: usize) -> Option<WebTemplateNode> {
compact_coded_text_with_other(&mut node.children);
compact_multiple_coded_texts(&mut node.children);
if !node.has_input() && node.children.len() == 1 && depth > 1 && is_skippable(&node.rm_type) {
let mut child = node.children.remove(0);
if node.rm_type == "ELEMENT" && (child.min.is_none() || child.min == Some(0)) {
child.min = Some(1);
}
copy_values(&node, &mut child);
Some(child)
} else if node.children.is_empty() && SKIP_IF_EMPTY.contains(&node.rm_type.as_str()) {
None
} else {
Some(node)
}
}
fn is_skippable(rm_type: &str) -> bool {
(rm_type.starts_with("DV_") && !rm_type.starts_with("DV_INTERVAL")) || rm_type == "ELEMENT"
}
fn compact_coded_text_with_other(children: &mut Vec<WebTemplateNode>) {
let [first, second] = children.as_slice() else {
return;
};
let rms = [first.rm_type.as_str(), second.rm_type.as_str()];
let is_pair = rms.contains(&"DV_TEXT") && rms.contains(&"DV_CODED_TEXT");
if is_pair && first.aql_path == second.aql_path {
let (coded, text) = if first.rm_type == "DV_CODED_TEXT" {
(0, 1)
} else {
(1, 0)
};
let Some(coded_child) = children.get_mut(coded) else {
return;
};
if let Some(input) = coded_child.inputs.first_mut() {
input.list_open = Some(true);
}
let mut other = WebTemplateInput::new(WebTemplateInputType::Text, Some("other"));
other.list_open = Some(true);
coded_child.inputs.push(other);
children.remove(text);
}
}
#[expect(
clippy::indexing_slicing,
reason = "`groups` holds `enumerate()` indices into `children` and nothing is removed before the indexed accesses (the `to_remove` list is applied after), so every index is in bounds by construction"
)]
fn compact_multiple_coded_texts(children: &mut Vec<WebTemplateNode>) {
let mut groups: IndexMap<String, Vec<usize>> = IndexMap::new();
for (i, child) in children.iter().enumerate() {
groups.entry(child.aql_path.clone()).or_default().push(i);
}
let mut to_remove: Vec<usize> = Vec::new();
let mut merges: Vec<(usize, usize)> = Vec::new();
for idxs in groups.values() {
let [a, b] = idxs[..] else { continue };
if !is_coded_text(&children[a]) || !is_coded_text(&children[b]) {
continue;
}
match (
is_input_constrained(&children[a]),
is_input_constrained(&children[b]),
) {
(true, false) => to_remove.push(b),
(false, true) => to_remove.push(a),
_ => merges.push((a, b)),
}
}
for (keep, drop) in merges {
union_coded_lists(children, keep, drop);
to_remove.push(drop);
}
to_remove.sort_unstable();
to_remove.dedup();
for i in to_remove.into_iter().rev() {
children.remove(i);
}
}
#[expect(
clippy::indexing_slicing,
reason = "both indices are `enumerate()` indices into `children` and nothing is removed before this runs, so they are in bounds by construction"
)]
fn union_coded_lists(children: &mut [WebTemplateNode], keep: usize, drop: usize) {
let drop_list = children[drop]
.inputs
.first()
.map(|i| i.list.clone())
.unwrap_or_default();
if let Some(input) = children[keep].inputs.first_mut() {
for cv in drop_list {
if !input.list.iter().any(|existing| existing.value == cv.value) {
input.list.push(cv);
}
}
}
}
fn is_coded_text(node: &WebTemplateNode) -> bool {
matches!(node.rm_type.as_str(), "DV_CODED_TEXT" | "CODE_PHRASE")
}
fn is_input_constrained(node: &WebTemplateNode) -> bool {
node.inputs.first().is_some_and(|i| i.validation.is_some())
}
fn copy_values(from: &WebTemplateNode, to: &mut WebTemplateNode) {
to.name.clone_from(&from.name);
to.localized_name.clone_from(&from.localized_name);
to.localized_names.clone_from(&from.localized_names);
for (k, v) in &from.localized_descriptions {
to.localized_descriptions
.entry(k.clone())
.or_insert_with(|| v.clone());
}
to.node_id.clone_from(&from.node_id);
to.name_code.clone_from(&from.name_code);
to.name_coded.clone_from(&from.name_coded);
let to_is_dv = to.rm_type.starts_with("DV_");
if to.min.is_none()
|| from.min.is_some_and(|fm| to.min.is_some_and(|tm| tm > fm))
|| (to_is_dv && to.min == Some(1))
{
to.min = from.min;
}
if from.max == -1 || (to.max != -1 && to.max < from.max) {
to.max = from.max;
}
for (k, v) in &from.annotations {
to.annotations.insert(k.clone(), v.clone());
}
for (k, v) in &from.term_bindings {
to.term_bindings.insert(k.clone(), v.clone());
}
to.existence.extend(from.existence.iter().cloned());
to.card_all.extend(from.card_all.iter().cloned());
to.slots.extend(from.slots.iter().cloned());
to.closed_attributes
.extend(from.closed_attributes.iter().cloned());
for binding in &from.constraint_bindings {
if !to.constraint_bindings.contains(binding) {
to.constraint_bindings.push(binding.clone());
}
}
}