use std::collections::HashMap;
use super::model::*;
pub fn postprocess(spec: &mut CimSpecification) {
determine_data_types(spec);
add_origins_of_attributes(spec);
set_profile_priorities(spec);
reorder_origins(spec);
set_main_origin(spec);
set_has_inverse_role(spec);
set_has_class_attributes(spec);
set_is_fixed_attributes(spec);
set_missing_namespaces(spec);
sort_attributes(spec);
set_is_inverse_role_attribute_list(spec);
rename_conflicting_attributes(spec);
remove_circular_dependencies(spec);
}
fn determine_data_types(spec: &mut CimSpecification) {
let dt_ids: Vec<String> = spec.cim_datatypes.keys().cloned().collect();
for id in &dt_ids {
let dt = spec.cim_datatypes.get(id).unwrap();
if dt.cim_stereotype == "Compound" {
continue;
}
let prim = dt
.attributes
.iter()
.find(|a| a.label == "value")
.map(|a| {
if a.cim_data_type == DATA_TYPE_DECIMAL {
DATA_TYPE_FLOAT.to_string()
} else {
a.cim_data_type.clone()
}
})
.unwrap_or_default();
spec.cim_datatypes.get_mut(id).unwrap().primitive_type = prim;
}
let enum_ids: std::collections::HashSet<String> = spec.enums.keys().cloned().collect();
let dt_ids_set: std::collections::HashSet<String> =
spec.cim_datatypes.keys().cloned().collect();
let dt_prim: HashMap<String, String> = spec
.cim_datatypes
.iter()
.map(|(k, v)| (k.clone(), v.primitive_type.clone()))
.collect();
for t in spec.types.values_mut() {
let mut prim_set: std::collections::HashSet<String> = Default::default();
let mut dt_set: std::collections::HashSet<String> = Default::default();
let mut enum_set: std::collections::HashSet<String> = Default::default();
for attr in &mut t.attributes {
if attr.cim_stereotype == "Primitive" || is_primitive_type(&attr.cim_data_type) {
attr.is_primitive = true;
attr.data_type = if attr.cim_data_type == DATA_TYPE_DECIMAL {
DATA_TYPE_FLOAT.to_string()
} else {
attr.cim_data_type.clone()
};
} else if attr.cim_stereotype == "CIMDatatype"
|| dt_ids_set.contains(&attr.cim_data_type)
{
attr.is_cim_datatype = true;
attr.data_type = dt_prim
.get(&attr.cim_data_type)
.cloned()
.unwrap_or_default();
} else if enum_ids.contains(&attr.rdf_range) {
attr.is_enum_value = true;
attr.data_type = attr.rdf_range.clone();
} else if !attr.is_list
&& (attr.cim_data_type == "Object" || attr.cim_data_type.is_empty())
{
attr.is_class = true;
attr.data_type = attr.rdf_range.clone();
} else if !attr.is_list && !attr.cim_data_type.is_empty() {
attr.is_class = true;
attr.data_type = attr.cim_data_type.clone();
} else {
attr.data_type = attr.rdf_range.clone();
}
if attr.is_primitive {
prim_set.insert(attr.data_type.clone());
} else if attr.is_cim_datatype {
dt_set.insert(attr.cim_data_type.clone());
} else if attr.is_enum_value {
enum_set.insert(attr.rdf_range.clone());
}
}
let mut prim_vec: Vec<String> = prim_set.into_iter().collect();
prim_vec.sort();
t.primitive_types = prim_vec;
let mut dt_vec: Vec<String> = dt_set.into_iter().collect();
dt_vec.sort();
t.cim_datatypes = dt_vec;
let mut enum_vec: Vec<String> = enum_set.into_iter().collect();
enum_vec.sort();
t.enum_types = enum_vec;
}
for p in spec.primitive_types.values_mut() {
p.data_type = match p.id.as_str() {
DATA_TYPE_STRING => DATA_TYPE_STRING.to_string(),
DATA_TYPE_INTEGER => DATA_TYPE_INTEGER.to_string(),
DATA_TYPE_BOOLEAN => DATA_TYPE_BOOLEAN.to_string(),
DATA_TYPE_FLOAT => DATA_TYPE_FLOAT.to_string(),
DATA_TYPE_DECIMAL => DATA_TYPE_FLOAT.to_string(),
_ => DATA_TYPE_STRING.to_string(),
};
}
}
fn is_primitive_type(s: &str) -> bool {
matches!(
s,
DATA_TYPE_STRING
| DATA_TYPE_INTEGER
| DATA_TYPE_BOOLEAN
| DATA_TYPE_FLOAT
| DATA_TYPE_DATE
| DATA_TYPE_DATE_TIME
| DATA_TYPE_MONTH_DAY
| "URI"
)
}
fn add_origins_of_attributes(spec: &mut CimSpecification) {
for t in spec.types.values_mut() {
let mut set: std::collections::HashSet<String> =
t.origins.iter().cloned().collect();
for attr in &t.attributes {
for o in &attr.origins {
set.insert(o.clone());
}
}
let mut v: Vec<String> = set.into_iter().collect();
v.sort();
t.origins = v;
}
}
fn set_profile_priorities(spec: &mut CimSpecification) {
let base = spec.family.base_profile;
if let Some(b) = base {
if let Some(o) = spec.ontologies.get_mut(b) {
o.priority = 1;
}
}
let mut others: Vec<String> = spec
.ontologies
.keys()
.filter(|k| Some(k.as_str()) != base)
.cloned()
.collect();
others.sort();
let mut p = if base.is_some() { 2u32 } else { 1u32 };
for k in others {
if let Some(o) = spec.ontologies.get_mut(&k) {
o.priority = p;
}
p += 1;
}
let mut list: Vec<(u32, String)> = spec
.ontologies
.iter()
.map(|(k, v)| (v.priority, k.clone()))
.collect();
list.sort_by_key(|(p, _)| *p);
spec.ontology_list = list.into_iter().map(|(_, k)| k).collect();
}
fn reorder_origins(spec: &mut CimSpecification) {
let base = spec.family.base_profile;
let prio: HashMap<String, u32> = spec
.ontologies
.iter()
.map(|(k, v)| (k.clone(), v.priority))
.collect();
for t in spec.types.values_mut() {
t.origins.sort_by_key(|o| prio.get(o).copied().unwrap_or(u32::MAX));
promote_defining_origin(t, base);
for attr in &mut t.attributes {
attr.origins
.sort_by_key(|o| prio.get(o).copied().unwrap_or(u32::MAX));
}
}
}
fn promote_defining_origin(t: &mut CimType, base: Option<&str>) {
if let Some(b) = base {
if t.origins.iter().any(|o| o == b) {
return;
}
}
let first_defines = t.origins.first().is_some_and(|o| t.concrete_in.contains(o));
if first_defines || t.concrete_in.is_empty() {
return;
}
if let Some(pos) = t.origins.iter().position(|o| t.concrete_in.contains(o)) {
let defining = t.origins.remove(pos);
t.origins.insert(0, defining);
}
}
fn set_main_origin(spec: &mut CimSpecification) {
let type_ids: Vec<String> = spec.types.keys().cloned().collect();
let base = spec.family.base_profile;
for id in type_ids {
let origin = compute_main_origin(&id, &spec.types, base);
if let Some(t) = spec.types.get_mut(&id) {
t.origin = origin;
}
}
}
fn compute_main_origin(
id: &str,
types: &HashMap<String, CimType>,
base: Option<&str>,
) -> String {
let mut counts: HashMap<String, usize> = HashMap::new();
let mut current_id = id.to_string();
loop {
let t = match types.get(¤t_id) {
Some(t) => t,
None => break,
};
for attr in &t.attributes {
if attr.origins.len() > 1 {
for o in &attr.origins {
if t.origins.contains(o) {
*counts.entry(o.clone()).or_insert(0) += 1;
}
}
}
}
if t.super_type.is_empty() || t.super_type == current_id {
break;
}
current_id = t.super_type.clone();
}
let t = match types.get(id) {
Some(t) => t,
None => return String::new(),
};
let candidates: Vec<String> = if counts.is_empty() {
t.origins.clone()
} else {
let max = *counts.values().max().unwrap_or(&0);
counts
.into_iter()
.filter(|(_, c)| *c == max)
.map(|(o, _)| o)
.collect()
};
if let Some(b) = base {
if candidates.iter().any(|c| c == b) {
return b.to_string();
}
}
let mut s = candidates;
s.sort();
s.into_iter().next().unwrap_or_default()
}
fn set_has_inverse_role(spec: &mut CimSpecification) {
for t in spec.types.values_mut() {
for attr in &mut t.attributes {
if !attr.cim_inverse_role.is_empty() {
attr.has_inverse_role = true;
if let Some((_, name)) = attr.cim_inverse_role.split_once('.') {
attr.inverse_role_attribute = name.to_string();
}
}
}
}
}
fn set_has_class_attributes(spec: &mut CimSpecification) {
for t in spec.types.values_mut() {
t.has_class_attributes = t
.attributes
.iter()
.any(|a| a.is_class && !a.is_list);
}
}
fn set_is_fixed_attributes(spec: &mut CimSpecification) {
for dt in spec.cim_datatypes.values_mut() {
for attr in &mut dt.attributes {
attr.is_fixed = !attr.cim_is_fixed.is_empty();
}
}
}
fn set_missing_namespaces(spec: &mut CimSpecification) {
let base = spec
.specification_namespaces
.get("base")
.cloned()
.unwrap_or_default();
for t in spec.types.values_mut() {
normalize_ns(&mut t.namespace, &base);
for attr in &mut t.attributes {
normalize_ns(&mut attr.namespace, &base);
}
}
for e in spec.enums.values_mut() {
normalize_ns(&mut e.namespace, &base);
}
let rev: HashMap<String, String> = spec
.specification_namespaces
.iter()
.filter(|(k, _)| k.as_str() != "base")
.map(|(k, v)| (v.clone(), k.clone()))
.collect();
for t in spec.types.values() {
if let Some(prefix) = rev.get(&t.namespace) {
spec.profile_namespaces
.insert(prefix.clone(), t.namespace.clone());
}
}
for e in spec.enums.values() {
if let Some(prefix) = rev.get(&e.namespace) {
spec.profile_namespaces
.insert(prefix.clone(), e.namespace.clone());
}
}
for (prefix, ns) in spec.family.extra_namespaces {
spec.specification_namespaces
.entry((*prefix).to_string())
.or_insert_with(|| (*ns).to_string());
spec.profile_namespaces
.entry((*prefix).to_string())
.or_insert_with(|| (*ns).to_string());
}
}
fn normalize_ns(ns: &mut String, base: &str) {
if !ns.ends_with('#') && !ns.ends_with('/') {
ns.push('#');
}
if ns.is_empty() || ns == "#" {
*ns = base.to_string();
}
}
fn sort_attributes(spec: &mut CimSpecification) {
for t in spec.types.values_mut() {
t.attributes.sort_by(|a, b| a.id.cmp(&b.id));
}
for dt in spec.cim_datatypes.values_mut() {
dt.attributes.sort_by(|a, b| a.id.cmp(&b.id));
}
}
fn set_is_inverse_role_attribute_list(spec: &mut CimSpecification) {
let mut flags: Vec<(String, usize)> = Vec::new();
for (tid, t) in &spec.types {
for (idx, attr) in t.attributes.iter().enumerate() {
if let Some((inv_type, inv_label)) = attr.cim_inverse_role.split_once('.') {
if let Some(inv_t) = spec.types.get(inv_type) {
if inv_t.attributes.iter().any(|a| a.label == inv_label && a.is_list) {
flags.push((tid.clone(), idx));
}
}
}
}
}
for (tid, idx) in flags {
if let Some(t) = spec.types.get_mut(&tid) {
t.attributes[idx].is_inverse_role_attribute_list = true;
}
}
}
fn rename_conflicting_attributes(spec: &mut CimSpecification) {
for t in spec.types.values_mut() {
for attr in &mut t.attributes {
if attr.label == "switch" {
attr.label = "switch_".to_string();
} else if attr.label == "IdentifiedObject" {
attr.label = "IdentifiedObject_".to_string();
}
}
}
}
fn remove_circular_dependencies(spec: &mut CimSpecification) {
for t in spec.types.values_mut() {
for attr in &mut t.attributes {
if !attr.is_primitive && !attr.is_enum_value && !attr.is_cim_datatype {
attr.use_id_reference = true;
}
}
}
}
#[cfg(test)]
mod tests {
use crate::family;
use crate::import::import_schema_files;
fn origins(spec: &crate::model::CimSpecification, class: &str) -> Vec<String> {
spec.types[class].origins.clone()
}
fn pattern_for(family: &family::Family) -> String {
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
format!("{root}/{}", family.default_schema)
}
#[test]
fn primary_origin_is_a_defining_profile() {
let spec =
import_schema_files(&pattern_for(&family::CGMES), &family::CGMES, false).unwrap();
assert_eq!(origins(&spec, "TopologicalNode"), ["TP", "SV"]);
assert_eq!(origins(&spec, "DCTopologicalNode"), ["TP", "SV"]);
assert_eq!(origins(&spec, "Equipment")[0], "EQ");
assert_eq!(origins(&spec, "Terminal")[0], "EQ");
assert_eq!(origins(&spec, "ConnectivityNode")[0], "EQ");
assert_eq!(origins(&spec, "SvVoltage")[0], "SV");
}
#[test]
fn nc_spec_parses_classes_enums_and_datatypes() {
let spec = import_schema_files(&pattern_for(&family::NC), &family::NC, false).unwrap();
assert_eq!(spec.types["Contingency"].namespace, "https://cim.ucaiug.io/ns#");
assert_eq!(
spec.types["OrdinaryContingency"].namespace,
"https://cim4.eu/ns/nc#"
);
assert_eq!(spec.types["OrdinaryContingency"].super_type, "Contingency");
assert!(spec.enums.contains_key("ContingencyEquipmentStatusKind"));
assert!(!spec.types.contains_key("ContingencyEquipmentStatusKind"));
assert!(
spec.types["Equipment"]
.attributes
.iter()
.any(|a| a.id == "Equipment.networkAnalysisEnabled"),
"NC Equipment should carry networkAnalysisEnabled"
);
assert_eq!(spec.ontologies["AE"].priority, 1);
}
}