fhir-diff 0.1.2

Semantic diff of FHIR packages and profiles with a breaking-change gate
Documentation
//! Compare each profile's effective tree with its published snapshot (min, max, mustSupport
//! of every element the tree has): `cargo run --example selfcheck -- <package>`.
use fhir_diff::{Node, Pool, load};

fn find<'a>(node: &'a Node, parts: &[&str]) -> Option<&'a Node> {
    let Some((head, rest)) = parts.split_first() else {
        return Some(node);
    };
    let (name, slice) = head
        .split_once(':')
        .map_or((*head, None), |(n, s)| (n, Some(s)));
    let name = name.trim_end_matches("[x]");
    let child = node.children.get(name)?;
    let child = match slice {
        Some(s) => child.slices.get(s)?,
        None => child,
    };
    find(child, rest)
}

fn main() {
    let spec = std::env::args().nth(1).expect("package");
    let loaded = load(&spec).expect("load");
    let pool = Pool::new(&loaded);
    let (mut checked, mut differ) = (0, 0);
    for profile in pool.profiles() {
        let Some(definition) = loaded
            .package
            .definitions
            .iter()
            .find(|d| d.file == profile.file)
        else {
            continue;
        };
        for element in definition.json["snapshot"]["element"]
            .as_array()
            .into_iter()
            .flatten()
        {
            let id = element["id"].as_str().unwrap_or_default();
            let parts: Vec<&str> = id.split('.').skip(1).collect();
            if parts.is_empty() || parts.iter().any(|p| p.contains("[x]:")) {
                continue;
            }
            let Some(node) = find(&profile.tree, &parts) else {
                continue;
            };
            checked += 1;
            let min = element["min"].as_u64().unwrap_or(0) as u32;
            let max = match element["max"].as_str() {
                Some("*") => None,
                Some(m) => m.parse().ok(),
                None => None,
            };
            let ms = element["mustSupport"].as_bool().unwrap_or(false);
            if node.min != min || node.max != max || node.must_support != ms {
                differ += 1;
                if differ <= 25 {
                    println!(
                        "{} {id}: tree {}..{:?} ms={} / snapshot {min}..{max:?} ms={ms}",
                        profile.name, node.min, node.max, node.must_support
                    );
                }
            }
        }
    }
    println!("checked {checked} elements, {differ} differ");
}