rh-foundation 0.2.0-beta.2

Foundation crate providing common utilities, error handling, and shared functionality
Documentation
use std::collections::HashMap;

use tracing::debug;

use crate::snapshot::error::{SnapshotError, SnapshotResult};
use crate::snapshot::merge_validation::{
    merge_binding, merge_cardinality, merge_constraints, merge_types,
};
use crate::snapshot::path::ElementPath;
use crate::snapshot::types::ElementDefinition;

pub struct ElementMerger;

impl ElementMerger {
    pub fn merge_elements(
        base: &[ElementDefinition],
        differential: &[ElementDefinition],
    ) -> SnapshotResult<Vec<ElementDefinition>> {
        debug!(
            "Merging {} base elements with {} differential elements",
            base.len(),
            differential.len()
        );

        let mut element_map = Self::create_element_map(base);
        Self::apply_differential(&mut element_map, differential)?;
        Self::expand_slice_children(&mut element_map);

        let mut result: Vec<ElementDefinition> = element_map.into_values().collect();
        Self::sort_elements_by_path(&mut result);

        debug!("Merge complete: {} elements in result", result.len());
        Ok(result)
    }

    fn create_element_map(
        base: &[ElementDefinition],
    ) -> HashMap<(String, Option<String>), ElementDefinition> {
        base.iter()
            .map(|element| {
                (
                    (element.path.clone(), element.slice_name.clone()),
                    element.clone(),
                )
            })
            .collect()
    }

    fn apply_differential(
        element_map: &mut HashMap<(String, Option<String>), ElementDefinition>,
        differential: &[ElementDefinition],
    ) -> SnapshotResult<()> {
        for diff_element in differential {
            let key = (diff_element.path.clone(), diff_element.slice_name.clone());

            if let Some(existing) = element_map.get(&key) {
                let merged = Self::merge_element(existing, diff_element)?;
                element_map.insert(key, merged);
            } else if diff_element.slice_name.is_some() {
                let base_key = (diff_element.path.clone(), None);
                if let Some(base_element) = element_map.get(&base_key) {
                    let merged = Self::merge_element(base_element, diff_element)?;
                    element_map.insert(key, merged);
                } else {
                    element_map.insert(key, diff_element.clone());
                }
            } else {
                element_map.insert(key, diff_element.clone());
            }
        }
        Ok(())
    }

    fn sort_elements_by_path(elements: &mut [ElementDefinition]) {
        elements.sort_by(|left, right| match left.path.cmp(&right.path) {
            std::cmp::Ordering::Equal => match (&left.slice_name, &right.slice_name) {
                (None, None) => std::cmp::Ordering::Equal,
                (None, Some(_)) => std::cmp::Ordering::Less,
                (Some(_), None) => std::cmp::Ordering::Greater,
                (Some(left_name), Some(right_name)) => left_name.cmp(right_name),
            },
            other => other,
        });
    }

    fn expand_slice_children(
        element_map: &mut HashMap<(String, Option<String>), ElementDefinition>,
    ) {
        let mut new_elements = Vec::new();

        let base_elements: Vec<(&String, ElementPath, &ElementDefinition)> = element_map
            .iter()
            .filter(|((_, slice_name), _)| slice_name.is_none())
            .map(|((path, _), element)| (path, ElementPath::new(path), element))
            .collect();

        let slice_roots: Vec<(String, String)> = element_map
            .keys()
            .filter_map(|(path, slice_name)| {
                slice_name.as_ref().map(|name| (path.clone(), name.clone()))
            })
            .collect();

        for (slice_path_str, slice_name) in slice_roots {
            let slice_base_path = ElementPath::new(&slice_path_str);

            for (path, element_path, element) in &base_elements {
                if element_path.is_child_of(&slice_base_path) {
                    let slice_child_key = ((*path).clone(), Some(slice_name.clone()));

                    if !element_map.contains_key(&slice_child_key) {
                        let mut new_element = (*element).clone();
                        new_element.slice_name = Some(slice_name.clone());
                        new_elements.push((slice_child_key, new_element));
                    }
                }
            }
        }

        for (key, element) in new_elements {
            element_map.insert(key, element);
        }
    }

    fn merge_element(
        base: &ElementDefinition,
        diff: &ElementDefinition,
    ) -> Result<ElementDefinition, SnapshotError> {
        let (merged_min, merged_max) = merge_cardinality(
            base.min,
            base.max.as_deref(),
            diff.min,
            diff.max.as_deref(),
            &diff.path,
        )?;

        let merged_type = merge_types(base.type_.as_ref(), diff.type_.as_ref(), &diff.path)?;
        let merged_binding =
            merge_binding(base.binding.as_ref(), diff.binding.as_ref(), &diff.path)?;
        let merged_constraints = merge_constraints(&base.constraint, &diff.constraint, &diff.path)?;

        Ok(ElementDefinition {
            path: diff.path.clone(),
            id: diff.id.clone().or_else(|| base.id.clone()),
            min: Some(merged_min),
            max: Some(merged_max),
            type_: merged_type,
            binding: merged_binding,
            constraint: merged_constraints,
            definition: diff.definition.clone().or_else(|| base.definition.clone()),
            short: diff.short.clone().or_else(|| base.short.clone()),
            comment: diff.comment.clone().or_else(|| base.comment.clone()),
            requirements: diff
                .requirements
                .clone()
                .or_else(|| base.requirements.clone()),
            must_support: diff.must_support.or(base.must_support),
            is_summary: diff.is_summary.or(base.is_summary),
            is_modifier: diff.is_modifier.or(base.is_modifier),
            is_modifier_reason: diff
                .is_modifier_reason
                .clone()
                .or_else(|| base.is_modifier_reason.clone()),
            slicing: diff.slicing.clone().or_else(|| base.slicing.clone()),
            slice_name: diff.slice_name.clone().or_else(|| base.slice_name.clone()),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn create_element(path: &str, slice_name: Option<&str>) -> ElementDefinition {
        ElementDefinition {
            path: path.to_string(),
            id: None,
            min: None,
            max: None,
            type_: None,
            binding: None,
            constraint: None,
            definition: None,
            short: None,
            comment: None,
            requirements: None,
            must_support: None,
            is_summary: None,
            is_modifier: None,
            is_modifier_reason: None,
            slicing: None,
            slice_name: slice_name.map(|slice| slice.to_string()),
        }
    }

    #[test]
    fn test_merge_elements_basic() {
        let base_elem = create_element("Patient", None);
        let diff_elem = create_element("Patient", None);

        let base = vec![base_elem];
        let diff = vec![diff_elem];

        let result = ElementMerger::merge_elements(&base, &diff).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].path, "Patient");
    }

    #[test]
    fn test_expand_slice_children() {
        let mut map = HashMap::new();

        let base_root = create_element("Patient.identifier", None);
        let base_child = create_element("Patient.identifier.system", None);

        map.insert(("Patient.identifier".to_string(), None), base_root);
        map.insert(("Patient.identifier.system".to_string(), None), base_child);

        let slice_root = create_element("Patient.identifier", Some("MRN"));
        map.insert(
            ("Patient.identifier".to_string(), Some("MRN".to_string())),
            slice_root,
        );

        ElementMerger::expand_slice_children(&mut map);

        assert!(map.contains_key(&(
            "Patient.identifier.system".to_string(),
            Some("MRN".to_string())
        )));

        let slice_child = map
            .get(&(
                "Patient.identifier.system".to_string(),
                Some("MRN".to_string()),
            ))
            .unwrap();
        assert_eq!(slice_child.slice_name.as_deref(), Some("MRN"));
    }
}