cimoxide-validation 0.3.1

SHACL validation of CGMES datasets against the ENTSO-E profile constraints
Documentation
use cimdecoder::CimDataset;
use crate::Violation;

pub fn validate(dataset: &CimDataset) -> Vec<Violation> {
    check_mutual_coupling_terminals_assignment(dataset)
}

fn check_mutual_coupling_terminals_assignment(dataset: &CimDataset) -> Vec<Violation> {
    let mut v = Vec::new();

    let conducting_equipment_of = |term_ref: &cimstructs::base::MridRef| -> Option<(String, Option<String>)> {
        let term_id = term_ref.mrid.trim_start_matches('#');
        let term_entry = dataset.entries.get(term_id)?;
        let term = term_entry.element.as_any().downcast_ref::<cimstructs::Terminal>()?;
        let ce_ref = term.conducting_equipment.as_ref()?;
        let eq_id = ce_ref.mrid.trim_start_matches('#').to_string();
        let type_name = dataset.entries.get(&eq_id).map(|e| e.element.type_name().to_string());
        Some((eq_id, type_name))
    };

    for mrid in dataset.by_type.get("MutualCoupling").into_iter().flatten() {
        let entry = &dataset.entries[mrid];
        let mc = match entry.element.as_any().downcast_ref::<cimstructs::MutualCoupling>() {
            Some(o) => o, None => continue,
        };
        let (eq1_id, eq1_type) = match mc.first_terminal.as_ref().and_then(|r| conducting_equipment_of(r)) {
            Some(x) => x, None => continue,
        };
        let (eq2_id, eq2_type) = match mc.second_terminal.as_ref().and_then(|r| conducting_equipment_of(r)) {
            Some(x) => x, None => continue,
        };

        let is_line_like = |t: &Option<String>| -> bool {
            matches!(t.as_deref(), Some("ACLineSegment") | Some("Equipment"))
        };

        if !is_line_like(&eq1_type) || !is_line_like(&eq2_type) || eq1_id == eq2_id {
            let t1 = eq1_type.as_deref().unwrap_or("unknown");
            let t2 = eq2_type.as_deref().unwrap_or("unknown");
            v.push(Violation {
                object_id:   mrid.clone(),
                rule_id:     "scn301:MutualCoupling-terminalsAssignment".into(),
                name:        "C:301:SC:MutualCoupling:terminalsAssignment".into(),
                class:       "MutualCoupling".into(),
                property:    "MutualCoupling.First_Terminal".into(),
                message:     format!("The terminals are either not related to ACLineSegment or the first and the second terminal associations are not pointing to different ACLineSegments. Type line 1: {t1}. Type line 2: {t2}."),
                severity:    "sh:Violation".into(),
                description: String::new(),
            });
        }
    }
    v
}