Skip to main content

cimvalidation/
detect.rs

1use cimmodel::CimDataset;
2use crate::Config;
3
4/// The short code for a CGMES profile IRI, from `CGMES/PROF` via
5/// [`crate::cgmes_profile_index`].
6fn uri_to_short_name(uri: &str) -> Option<&'static str> {
7    crate::cgmes_profile_index().0.iter().find(|(iri, _)| *iri == uri).map(|(_, code)| *code)
8}
9
10fn collect_profiles_from_type(dataset: &CimDataset, type_name: &str, seen: &mut std::collections::HashSet<&'static str>) {
11    for mrid in dataset.by_type.get(type_name).into_iter().flatten() {
12        let entry = match dataset.entries.get(mrid) { Some(e) => e, None => continue };
13        let Some(model) = crate::sparql::Fields::of_class(entry, type_name) else { continue };
14        for p in model.texts("Model.profile") {
15            let p = p.trim();
16            if !p.is_empty()
17                && let Some(short) = uri_to_short_name(p) {
18                    seen.insert(short);
19                }
20        }
21    }
22}
23
24/// Inspects dataset model headers and returns a Config with profiles, solved, and not_solved populated.
25pub fn detect_config(dataset: &CimDataset) -> Config {
26    let mut seen: std::collections::HashSet<&'static str> = std::collections::HashSet::new();
27    collect_profiles_from_type(dataset, "FullModel", &mut seen);
28    collect_profiles_from_type(dataset, "DifferenceModel", &mut seen);
29
30    let mut profiles: Vec<String> = seen.iter().map(|s| s.to_string()).collect();
31    profiles.sort();
32
33    let is_solved = seen.contains("SV");
34    Config {
35        profiles,
36        solved: is_solved,
37        not_solved: !is_solved,
38        ..Config::default()
39    }
40}
41
42// ── NC / property-bag families ─────────────────────────────────────────────
43
44/// Profile codes an NC dataset declares conformance to.
45///
46/// CGMES announces its profiles in an `md:FullModel` header whose
47/// `Model.profile` values are matched against `CGMES/PROF`'s index above. NC
48/// uses a DCAT header instead: a `dcat:Dataset` with `dcterms:conformsTo`
49/// naming profile IRIs, which `NCP/PROF` maps to short codes.
50///
51/// The field key is the bare `conformsTo`, because the decoder keys fields by
52/// the XML local name and the predicate is written `<dcterms:conformsTo>`.
53pub fn detect_nc_profiles(dataset: &CimDataset) -> Vec<String> {
54    let mut seen: Vec<String> = Vec::new();
55    for type_name in ["nc:Dataset", "nc:DifferenceSet"] {
56        for mrid in dataset.by_type.get(type_name).into_iter().flatten() {
57            let Some(el) = dataset.entries.get(mrid) else { continue };
58            // Written as rdf:resource in practice, but a plain literal is
59            // legal too, so both are read.
60            let refs: Vec<&str> = el.get_refs("conformsTo").iter().map(String::as_str).collect();
61            let text = el.get_str("conformsTo").into_iter().collect::<Vec<_>>();
62            for iri in refs.into_iter().chain(text) {
63                if let Some(code) = nc_profile_code(iri)
64                    && !seen.iter().any(|s| s == code) {
65                        seen.push(code.to_string());
66                    }
67            }
68        }
69    }
70    seen.sort();
71    seen
72}
73
74/// The short code for a profile IRI, if it names one.
75///
76/// A dataset may declare the base IRI or a version IRI; the descriptors list
77/// both. An IRI the descriptors do not know is not an NC profile.
78pub fn nc_profile_code(iri: &str) -> Option<&'static str> {
79    let iri = iri.trim();
80    crate::nc_profile_index()
81        .0
82        .iter()
83        .find(|(known, _)| *known == iri)
84        .map(|(_, code)| *code)
85}