use std::collections::{HashMap, HashSet};
use super::model::WebTemplateNode;
fn generate_node_id(name: &str) -> String {
let mut normalised = String::with_capacity(name.len());
for ch in name.chars() {
if ch.is_alphabetic() || ch.is_ascii_digit() || ch == '_' || ch == '.' || ch == '-' {
normalised.extend(ch.to_lowercase());
} else {
normalised.push('_');
}
}
let mut collapsed = String::with_capacity(normalised.len());
let mut prev_underscore = false;
for ch in normalised.chars() {
if ch == '_' {
if !prev_underscore {
collapsed.push('_');
}
prev_underscore = true;
} else {
collapsed.push(ch);
prev_underscore = false;
}
}
let trimmed = collapsed.trim_matches('_');
if trimmed.is_empty() {
return "id".to_owned();
}
if trimmed.starts_with(|c: char| c.is_ascii_digit()) {
format!("a{trimmed}")
} else {
trimmed.to_owned()
}
}
#[derive(Default)]
struct Deduplicator {
used: HashMap<String, HashSet<String>>,
}
impl Deduplicator {
fn unique(&mut self, parent_id: &str, base: &str) -> String {
let set = self.used.entry(parent_id.to_owned()).or_default();
if set.insert(base.to_owned()) {
return base.to_owned();
}
let mut i = 1_usize;
loop {
let candidate = format!("{base}_{i}");
if set.insert(candidate.clone()) {
return candidate;
}
i += 1;
}
}
}
fn typed_id(rm_type: &str) -> String {
if let Some(rest) = rm_type
.strip_prefix("DV_INTERVAL<DV_")
.and_then(|s| s.strip_suffix('>'))
{
format!("interval_of_{}_value", rest.to_lowercase())
} else if let Some(rest) = rm_type.strip_prefix("DV_") {
format!("{}_value", rest.to_lowercase())
} else {
format!("{}_value", rm_type.to_lowercase())
}
}
fn base_name(node: &WebTemplateNode, parent_rm_type: Option<&str>) -> String {
match &node.name {
Some(n) if !n.trim().is_empty() => n.clone(),
_ => {
if parent_rm_type == Some("ELEMENT") && !node.rm_type.starts_with("DV_INTERVAL") {
"value".to_owned()
} else {
last_path_segment(&node.aql_path)
}
}
}
}
fn last_path_segment(path: &str) -> String {
match path.rsplit_once('/') {
Some((_, last)) => last.to_owned(),
None => path.to_owned(),
}
}
fn fix_polymorphic_order(children: &mut Vec<WebTemplateNode>) {
let coded = children.iter().position(|c| c.rm_type == "DV_CODED_TEXT");
let text = children.iter().position(|c| c.rm_type == "DV_TEXT");
if let (Some(coded), Some(text)) = (coded, text)
&& coded > text
{
let node = children.remove(coded);
children.insert(text, node);
}
}
pub(crate) fn build_ids(root: &mut WebTemplateNode) {
let mut dedup = Deduplicator::default();
assign(root, "", &mut dedup, None, None);
}
fn assign(
node: &mut WebTemplateNode,
parent_prefix: &str,
dedup: &mut Deduplicator,
forced_base: Option<String>,
parent_rm_type: Option<&str>,
) {
let raw_name = forced_base.unwrap_or_else(|| base_name(node, parent_rm_type));
let base = generate_node_id(&raw_name);
let id = dedup.unique(parent_prefix, &base);
node.full_id = format!("{parent_prefix}{id}");
node.id = id;
let is_choice = node.rm_type == "ELEMENT" && node.children.len() > 1;
if is_choice {
fix_polymorphic_order(&mut node.children);
}
let child_prefix = format!("{}/", node.full_id);
let rm_type = node.rm_type.clone();
for (index, child) in node.children.iter_mut().enumerate() {
let fb = if is_choice && child.rm_type.starts_with("DV_") {
child.alt_json_id = Some(if index > 0 {
format!("value{}", index + 1)
} else {
"value".to_owned()
});
Some(typed_id(&child.rm_type))
} else {
None
};
assign(child, &child_prefix, dedup, fb, Some(&rm_type));
}
resolve_cardinalities(node);
resolve_depends_on(node);
}
fn resolve_cardinalities(node: &mut WebTemplateNode) {
if node.cardinalities.is_empty() {
return;
}
let child_ids: Vec<(String, String)> = node
.children
.iter()
.map(|c| (c.aql_path.clone(), c.id.clone()))
.collect();
node.cardinalities.retain_mut(|card| {
let ids: Vec<String> = child_ids
.iter()
.filter(|(path, _)| path.starts_with(&card.path))
.map(|(_, id)| id.clone())
.collect();
if ids.is_empty() {
false
} else {
card.ids = Some(ids);
true
}
});
}
fn resolve_depends_on(node: &mut WebTemplateNode) {
let siblings: Vec<(String, String, bool)> = node
.children
.iter()
.map(|c| (c.aql_path.clone(), c.id.clone(), c.in_context == Some(true)))
.collect();
for child in &mut node.children {
let Some(paths) = child.depends_on.take() else {
continue;
};
let mut ids: Vec<String> = Vec::new();
for path in &paths {
for (spath, sid, in_context) in &siblings {
if !in_context && spath.starts_with(path) && !ids.contains(sid) {
ids.push(sid.clone());
}
}
}
child.depends_on = if ids.is_empty() { None } else { Some(ids) };
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn node_id_generation_examples() {
assert_eq!(generate_node_id("Body temperature"), "body_temperature");
assert_eq!(generate_node_id("Problem/diagnosis"), "problem_diagnosis");
assert_eq!(generate_node_id("Tests (1, 2, 3)"), "tests_1_2_3");
assert_eq!(generate_node_id("1st visit"), "a1st_visit");
assert_eq!(generate_node_id("Blood Pressure"), "blood_pressure");
}
#[test]
fn empty_result_becomes_id() {
assert_eq!(generate_node_id(""), "id");
assert_eq!(generate_node_id("()"), "id");
assert_eq!(generate_node_id("___"), "id");
}
#[test]
fn preserves_allowed_punctuation_and_consolidates_underscores() {
assert_eq!(generate_node_id("keeps.dot-dash"), "keeps.dot-dash");
assert_eq!(generate_node_id("__weird__name__"), "weird_name");
assert_eq!(generate_node_id(" a / b "), "a_b");
}
#[test]
fn duplicate_siblings_get_numeric_suffix() {
let mut dedup = Deduplicator::default();
assert_eq!(dedup.unique("root", "blood_pressure"), "blood_pressure");
assert_eq!(dedup.unique("root", "blood_pressure"), "blood_pressure_1");
assert_eq!(dedup.unique("root", "blood_pressure"), "blood_pressure_2");
assert_eq!(dedup.unique("other", "blood_pressure"), "blood_pressure");
}
#[test]
fn typed_ids_for_polymorphic_values() {
assert_eq!(typed_id("DV_QUANTITY"), "quantity_value");
assert_eq!(typed_id("DV_CODED_TEXT"), "coded_text_value");
assert_eq!(
typed_id("DV_INTERVAL<DV_QUANTITY>"),
"interval_of_quantity_value"
);
}
}