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
21fn determine_data_types(spec: &mut CimSpecification) {
23 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 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 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 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
141fn 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
157fn 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 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
190fn 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 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
238fn 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(¤t_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
304fn 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
318fn 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
328fn 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
337fn 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 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 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
396fn 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
406fn set_is_inverse_role_attribute_list(spec: &mut CimSpecification) {
408 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
428fn 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
441fn 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 assert_eq!(origins(&spec, "TopologicalNode"), ["TP", "SV"]);
473 assert_eq!(origins(&spec, "DCTopologicalNode"), ["TP", "SV"]);
474 assert_eq!(origins(&spec, "Equipment")[0], "EQ");
477 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 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 assert!(spec.enums.contains_key("ContingencyEquipmentStatusKind"));
499 assert!(!spec.types.contains_key("ContingencyEquipmentStatusKind"));
500
501 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 assert_eq!(spec.ontologies["AE"].priority, 1);
512 }
513}