Skip to main content

cimschema/
processing.rs

1use std::collections::HashMap;
2
3use super::model::*;
4
5pub fn postprocess(spec: &mut CimSpecification) {
6    determine_data_types(spec);
7    add_origins_of_attributes(spec);
8    set_profile_priorities(spec);
9    reorder_origins(spec);
10    set_main_origin(spec);
11    set_has_inverse_role(spec);
12    set_has_class_attributes(spec);
13    set_is_fixed_attributes(spec);
14    set_missing_namespaces(spec);
15    sort_attributes(spec);
16    set_is_inverse_role_attribute_list(spec);
17    rename_conflicting_attributes(spec);
18    remove_circular_dependencies(spec);
19}
20
21// Pass 1: classify every attribute as primitive / CIMDatatype / enum / class.
22fn determine_data_types(spec: &mut CimSpecification) {
23    // First: resolve primitive_type for each CIMDatatype via its "value" attribute.
24    let dt_ids: Vec<String> = spec.cim_datatypes.keys().cloned().collect();
25    for id in &dt_ids {
26        let dt = spec.cim_datatypes.get(id).unwrap();
27        if dt.cim_stereotype == "Compound" {
28            continue;
29        }
30        let prim = dt
31            .attributes
32            .iter()
33            .find(|a| a.label == "value")
34            .map(|a| {
35                if a.cim_data_type == DATA_TYPE_DECIMAL {
36                    DATA_TYPE_FLOAT.to_string()
37                } else {
38                    a.cim_data_type.clone()
39                }
40            })
41            .unwrap_or_default();
42        spec.cim_datatypes.get_mut(id).unwrap().primitive_type = prim;
43    }
44
45    // Collect sets we need for look-up (avoid borrowing spec mutably while reading it)
46    let enum_ids: std::collections::HashSet<String> = spec.enums.keys().cloned().collect();
47    let dt_ids_set: std::collections::HashSet<String> =
48        spec.cim_datatypes.keys().cloned().collect();
49    let dt_prim: HashMap<String, String> = spec
50        .cim_datatypes
51        .iter()
52        .map(|(k, v)| (k.clone(), v.primitive_type.clone()))
53        .collect();
54
55    // Then: classify each attribute in every CimType.
56    for t in spec.types.values_mut() {
57        let mut prim_set: std::collections::HashSet<String> = Default::default();
58        let mut dt_set: std::collections::HashSet<String> = Default::default();
59        let mut enum_set: std::collections::HashSet<String> = Default::default();
60
61        for attr in &mut t.attributes {
62            if attr.cim_stereotype == "Primitive" || is_primitive_type(&attr.cim_data_type) {
63                attr.is_primitive = true;
64                attr.data_type = if attr.cim_data_type == DATA_TYPE_DECIMAL {
65                    DATA_TYPE_FLOAT.to_string()
66                } else {
67                    attr.cim_data_type.clone()
68                };
69            } else if attr.cim_stereotype == "CIMDatatype"
70                || dt_ids_set.contains(&attr.cim_data_type)
71            {
72                attr.is_cim_datatype = true;
73                attr.data_type = dt_prim
74                    .get(&attr.cim_data_type)
75                    .cloned()
76                    .unwrap_or_default();
77            } else if enum_ids.contains(&attr.rdf_range) {
78                attr.is_enum_value = true;
79                attr.data_type = attr.rdf_range.clone();
80            } else if !attr.is_list
81                && (attr.cim_data_type == "Object" || attr.cim_data_type.is_empty())
82            {
83                attr.is_class = true;
84                attr.data_type = attr.rdf_range.clone();
85            } else if !attr.is_list && !attr.cim_data_type.is_empty() {
86                attr.is_class = true;
87                attr.data_type = attr.cim_data_type.clone();
88            } else {
89                attr.data_type = attr.rdf_range.clone();
90            }
91
92            if attr.is_primitive {
93                prim_set.insert(attr.data_type.clone());
94            } else if attr.is_cim_datatype {
95                dt_set.insert(attr.cim_data_type.clone());
96            } else if attr.is_enum_value {
97                enum_set.insert(attr.rdf_range.clone());
98            }
99        }
100
101        let mut prim_vec: Vec<String> = prim_set.into_iter().collect();
102        prim_vec.sort();
103        t.primitive_types = prim_vec;
104
105        let mut dt_vec: Vec<String> = dt_set.into_iter().collect();
106        dt_vec.sort();
107        t.cim_datatypes = dt_vec;
108
109        let mut enum_vec: Vec<String> = enum_set.into_iter().collect();
110        enum_vec.sort();
111        t.enum_types = enum_vec;
112    }
113
114    // Primitives: map known ids to data types.
115    for p in spec.primitive_types.values_mut() {
116        p.data_type = match p.id.as_str() {
117            DATA_TYPE_STRING => DATA_TYPE_STRING.to_string(),
118            DATA_TYPE_INTEGER => DATA_TYPE_INTEGER.to_string(),
119            DATA_TYPE_BOOLEAN => DATA_TYPE_BOOLEAN.to_string(),
120            DATA_TYPE_FLOAT => DATA_TYPE_FLOAT.to_string(),
121            DATA_TYPE_DECIMAL => DATA_TYPE_FLOAT.to_string(),
122            _ => DATA_TYPE_STRING.to_string(),
123        };
124    }
125}
126
127fn is_primitive_type(s: &str) -> bool {
128    matches!(
129        s,
130        DATA_TYPE_STRING
131            | DATA_TYPE_INTEGER
132            | DATA_TYPE_BOOLEAN
133            | DATA_TYPE_FLOAT
134            | DATA_TYPE_DATE
135            | DATA_TYPE_DATE_TIME
136            | DATA_TYPE_MONTH_DAY
137            | "URI"
138    )
139}
140
141// Pass 2: merge attribute origins up into the owning type's origins.
142fn add_origins_of_attributes(spec: &mut CimSpecification) {
143    for t in spec.types.values_mut() {
144        let mut set: std::collections::HashSet<String> =
145            t.origins.iter().cloned().collect();
146        for attr in &t.attributes {
147            for o in &attr.origins {
148                set.insert(o.clone());
149            }
150        }
151        let mut v: Vec<String> = set.into_iter().collect();
152        v.sort();
153        t.origins = v;
154    }
155}
156
157// Pass 3: assign priorities — the family's base profile first (CGMES: EQ), the
158// rest alphabetical. A family without a base profile is purely alphabetical.
159fn set_profile_priorities(spec: &mut CimSpecification) {
160    let base = spec.family.base_profile;
161    if let Some(b) = base {
162        if let Some(o) = spec.ontologies.get_mut(b) {
163            o.priority = 1;
164        }
165    }
166    let mut others: Vec<String> = spec
167        .ontologies
168        .keys()
169        .filter(|k| Some(k.as_str()) != base)
170        .cloned()
171        .collect();
172    others.sort();
173    let mut p = if base.is_some() { 2u32 } else { 1u32 };
174    for k in others {
175        if let Some(o) = spec.ontologies.get_mut(&k) {
176            o.priority = p;
177        }
178        p += 1;
179    }
180    // Build ordered list
181    let mut list: Vec<(u32, String)> = spec
182        .ontologies
183        .iter()
184        .map(|(k, v)| (v.priority, k.clone()))
185        .collect();
186    list.sort_by_key(|(p, _)| *p);
187    spec.ontology_list = list.into_iter().map(|(_, k)| k).collect();
188}
189
190// Pass 4: sort origins by priority, then make sure `origins[0]` (the primary
191// profile, the one that writes a class with `rdf:ID`, mRID and name) is a
192// profile that defines the class. Priority alone is EQ first, then
193// alphabetical, which picks a profile that merely references the class when
194// the defining profile sorts later: `TopologicalNode` and
195// `DCTopologicalNode` appear in SV and TP, are concrete only in TP, and were
196// otherwise written to TP as bare `rdf:about` references. Classes EQ declares
197// keep EQ first (see `promote_defining_origin`).
198fn reorder_origins(spec: &mut CimSpecification) {
199    let base = spec.family.base_profile;
200    let prio: HashMap<String, u32> = spec
201        .ontologies
202        .iter()
203        .map(|(k, v)| (k.clone(), v.priority))
204        .collect();
205
206    for t in spec.types.values_mut() {
207        t.origins.sort_by_key(|o| prio.get(o).copied().unwrap_or(u32::MAX));
208        promote_defining_origin(t, base);
209        for attr in &mut t.attributes {
210            attr.origins
211                .sort_by_key(|o| prio.get(o).copied().unwrap_or(u32::MAX));
212        }
213    }
214}
215
216fn promote_defining_origin(t: &mut CimType, base: Option<&str>) {
217    // A base profile defines every class it declares, abstract ones included:
218    // CGMES's SSH declares `Equipment` concrete only so that
219    // `<cim:Equipment rdf:about=...>` can carry `Equipment.inService` for
220    // equipment defined in EQ, and must keep referencing it. Only a class the
221    // base profile does not declare at all takes its defining profile from the
222    // `concrete` stereotype. A family without a base profile always does.
223    if let Some(b) = base {
224        if t.origins.iter().any(|o| o == b) {
225            return;
226        }
227    }
228    let first_defines = t.origins.first().is_some_and(|o| t.concrete_in.contains(o));
229    if first_defines || t.concrete_in.is_empty() {
230        return;
231    }
232    if let Some(pos) = t.origins.iter().position(|o| t.concrete_in.contains(o)) {
233        let defining = t.origins.remove(pos);
234        t.origins.insert(0, defining);
235    }
236}
237
238// Pass 5: select the dominant origin per type.
239fn set_main_origin(spec: &mut CimSpecification) {
240    let type_ids: Vec<String> = spec.types.keys().cloned().collect();
241    let base = spec.family.base_profile;
242    for id in type_ids {
243        let origin = compute_main_origin(&id, &spec.types, base);
244        if let Some(t) = spec.types.get_mut(&id) {
245            t.origin = origin;
246        }
247    }
248}
249
250fn compute_main_origin(
251    id: &str,
252    types: &HashMap<String, CimType>,
253    base: Option<&str>,
254) -> String {
255    let mut counts: HashMap<String, usize> = HashMap::new();
256    let mut current_id = id.to_string();
257
258    loop {
259        let t = match types.get(&current_id) {
260            Some(t) => t,
261            None => break,
262        };
263        for attr in &t.attributes {
264            if attr.origins.len() > 1 {
265                for o in &attr.origins {
266                    if t.origins.contains(o) {
267                        *counts.entry(o.clone()).or_insert(0) += 1;
268                    }
269                }
270            }
271        }
272        if t.super_type.is_empty() || t.super_type == current_id {
273            break;
274        }
275        current_id = t.super_type.clone();
276    }
277
278    let t = match types.get(id) {
279        Some(t) => t,
280        None => return String::new(),
281    };
282
283    let candidates: Vec<String> = if counts.is_empty() {
284        t.origins.clone()
285    } else {
286        let max = *counts.values().max().unwrap_or(&0);
287        counts
288            .into_iter()
289            .filter(|(_, c)| *c == max)
290            .map(|(o, _)| o)
291            .collect()
292    };
293
294    if let Some(b) = base {
295        if candidates.iter().any(|c| c == b) {
296            return b.to_string();
297        }
298    }
299    let mut s = candidates;
300    s.sort();
301    s.into_iter().next().unwrap_or_default()
302}
303
304// Pass 6: populate has_inverse_role and inverse_role_attribute.
305fn set_has_inverse_role(spec: &mut CimSpecification) {
306    for t in spec.types.values_mut() {
307        for attr in &mut t.attributes {
308            if !attr.cim_inverse_role.is_empty() {
309                attr.has_inverse_role = true;
310                if let Some((_, name)) = attr.cim_inverse_role.split_once('.') {
311                    attr.inverse_role_attribute = name.to_string();
312                }
313            }
314        }
315    }
316}
317
318// Pass 7: mark types that have class-valued (non-list) attributes.
319fn set_has_class_attributes(spec: &mut CimSpecification) {
320    for t in spec.types.values_mut() {
321        t.has_class_attributes = t
322            .attributes
323            .iter()
324            .any(|a| a.is_class && !a.is_list);
325    }
326}
327
328// Pass 8: mark fixed attributes in CIMDatatypes.
329fn set_is_fixed_attributes(spec: &mut CimSpecification) {
330    for dt in spec.cim_datatypes.values_mut() {
331        for attr in &mut dt.attributes {
332            attr.is_fixed = !attr.cim_is_fixed.is_empty();
333        }
334    }
335}
336
337// Pass 9: fill missing namespaces; build profile_namespaces.
338fn set_missing_namespaces(spec: &mut CimSpecification) {
339    let base = spec
340        .specification_namespaces
341        .get("base")
342        .cloned()
343        .unwrap_or_default();
344
345    for t in spec.types.values_mut() {
346        normalize_ns(&mut t.namespace, &base);
347        for attr in &mut t.attributes {
348            normalize_ns(&mut attr.namespace, &base);
349        }
350    }
351    for e in spec.enums.values_mut() {
352        normalize_ns(&mut e.namespace, &base);
353    }
354
355    // Build reverse map ns_url → prefix
356    let rev: HashMap<String, String> = spec
357        .specification_namespaces
358        .iter()
359        .filter(|(k, _)| k.as_str() != "base")
360        .map(|(k, v)| (v.clone(), k.clone()))
361        .collect();
362
363    for t in spec.types.values() {
364        if let Some(prefix) = rev.get(&t.namespace) {
365            spec.profile_namespaces
366                .insert(prefix.clone(), t.namespace.clone());
367        }
368    }
369    for e in spec.enums.values() {
370        if let Some(prefix) = rev.get(&e.namespace) {
371            spec.profile_namespaces
372                .insert(prefix.clone(), e.namespace.clone());
373        }
374    }
375
376    // Namespaces the family always needs, whether or not a class lives in them.
377    for (prefix, ns) in spec.family.extra_namespaces {
378        spec.specification_namespaces
379            .entry((*prefix).to_string())
380            .or_insert_with(|| (*ns).to_string());
381        spec.profile_namespaces
382            .entry((*prefix).to_string())
383            .or_insert_with(|| (*ns).to_string());
384    }
385}
386
387fn normalize_ns(ns: &mut String, base: &str) {
388    if !ns.ends_with('#') && !ns.ends_with('/') {
389        ns.push('#');
390    }
391    if ns.is_empty() || ns == "#" {
392        *ns = base.to_string();
393    }
394}
395
396// Pass 10: sort attributes by id within each type.
397fn sort_attributes(spec: &mut CimSpecification) {
398    for t in spec.types.values_mut() {
399        t.attributes.sort_by(|a, b| a.id.cmp(&b.id));
400    }
401    for dt in spec.cim_datatypes.values_mut() {
402        dt.attributes.sort_by(|a, b| a.id.cmp(&b.id));
403    }
404}
405
406// Pass 11: set is_inverse_role_attribute_list.
407fn set_is_inverse_role_attribute_list(spec: &mut CimSpecification) {
408    // Collect (type_id, attr_idx) pairs that should be flagged, then apply.
409    let mut flags: Vec<(String, usize)> = Vec::new();
410    for (tid, t) in &spec.types {
411        for (idx, attr) in t.attributes.iter().enumerate() {
412            if let Some((inv_type, inv_label)) = attr.cim_inverse_role.split_once('.') {
413                if let Some(inv_t) = spec.types.get(inv_type) {
414                    if inv_t.attributes.iter().any(|a| a.label == inv_label && a.is_list) {
415                        flags.push((tid.clone(), idx));
416                    }
417                }
418            }
419        }
420    }
421    for (tid, idx) in flags {
422        if let Some(t) = spec.types.get_mut(&tid) {
423            t.attributes[idx].is_inverse_role_attribute_list = true;
424        }
425    }
426}
427
428// Pass 12: rename labels that conflict with language keywords.
429fn rename_conflicting_attributes(spec: &mut CimSpecification) {
430    for t in spec.types.values_mut() {
431        for attr in &mut t.attributes {
432            if attr.label == "switch" {
433                attr.label = "switch_".to_string();
434            } else if attr.label == "IdentifiedObject" {
435                attr.label = "IdentifiedObject_".to_string();
436            }
437        }
438    }
439}
440
441// Pass 13: mark all non-primitive, non-enum, non-CIMDatatype attributes as ID references.
442fn remove_circular_dependencies(spec: &mut CimSpecification) {
443    for t in spec.types.values_mut() {
444        for attr in &mut t.attributes {
445            if !attr.is_primitive && !attr.is_enum_value && !attr.is_cim_datatype {
446                attr.use_id_reference = true;
447            }
448        }
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use crate::family;
455    use crate::import::import_schema_files;
456
457    fn origins(spec: &crate::model::CimSpecification, class: &str) -> Vec<String> {
458        spec.types[class].origins.clone()
459    }
460
461    fn pattern_for(family: &family::Family) -> String {
462        let root = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
463        format!("{root}/{}", family.default_schema)
464    }
465
466    #[test]
467    fn primary_origin_is_a_defining_profile() {
468        let spec =
469            import_schema_files(&pattern_for(&family::CGMES), &family::CGMES, false).unwrap();
470
471        // Defined in TP, only referenced from SV.
472        assert_eq!(origins(&spec, "TopologicalNode"), ["TP", "SV"]);
473        assert_eq!(origins(&spec, "DCTopologicalNode"), ["TP", "SV"]);
474        // Declared by EQ (abstract there, concrete in SSH only as the carrier of
475        // Equipment.inService): EQ stays primary, so SSH keeps writing rdf:about.
476        assert_eq!(origins(&spec, "Equipment")[0], "EQ");
477        // Already primary in a defining profile: unchanged.
478        assert_eq!(origins(&spec, "Terminal")[0], "EQ");
479        assert_eq!(origins(&spec, "ConnectivityNode")[0], "EQ");
480        assert_eq!(origins(&spec, "SvVoltage")[0], "SV");
481    }
482
483    #[test]
484    fn nc_spec_parses_classes_enums_and_datatypes() {
485        let spec = import_schema_files(&pattern_for(&family::NC), &family::NC, false).unwrap();
486
487        // NCP declares classes in two namespaces and inherits across them.
488        assert_eq!(spec.types["Contingency"].namespace, "https://cim.ucaiug.io/ns#");
489        assert_eq!(
490            spec.types["OrdinaryContingency"].namespace,
491            "https://cim4.eu/ns/nc#"
492        );
493        assert_eq!(spec.types["OrdinaryContingency"].super_type, "Contingency");
494
495        // Regression guard for the stereotype ordering: NCP writes the
496        // classifier first and trails an "NC" tag, which last-wins misreads as
497        // a plain class.
498        assert!(spec.enums.contains_key("ContingencyEquipmentStatusKind"));
499        assert!(!spec.types.contains_key("ContingencyEquipmentStatusKind"));
500
501        // NC's Equipment is not CGMES's Equipment.
502        assert!(
503            spec.types["Equipment"]
504                .attributes
505                .iter()
506                .any(|a| a.id == "Equipment.networkAnalysisEnabled"),
507            "NC Equipment should carry networkAnalysisEnabled"
508        );
509
510        // No base profile: priorities are purely alphabetical from 1.
511        assert_eq!(spec.ontologies["AE"].priority, 1);
512    }
513}