cimoxide-validation 0.6.1

SHACL validation of CGMES datasets against the ENTSO-E profile constraints
Documentation
use cimmodel::CimDataset;
use super::Fields;
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: &str| -> Option<(String, Option<String>)> {
        let term_id = term_ref.trim_start_matches('#');
        let term = Fields::get(dataset, term_id, "Terminal")?;
        let ce_ref = term.reference("Terminal.ConductingEquipment")?;
        let eq_id = ce_ref.trim_start_matches('#').to_string();
        let type_name = dataset.entries.get(&eq_id).map(|e| e.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 Fields::of_class(entry, "MutualCoupling") {
            Some(o) => o, None => continue,
        };
        let (eq1_id, eq1_type) = match mc.reference("MutualCoupling.First_Terminal").and_then(conducting_equipment_of) {
            Some(x) => x, None => continue,
        };
        let (eq2_id, eq2_type) = match mc.reference("MutualCoupling.Second_Terminal").and_then(conducting_equipment_of) {
            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
}