use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::fmt::Write as FmtWrite;
use std::sync::OnceLock;
use crate::CimDataset;
use crate::base::{Element, FieldValue};
use crate::base::{AttrKind, Schema};
use crate::registry::type_registry;
const FALLBACK_PREFIX: &str = "cim";
#[derive(Clone, Copy)]
struct AttrMeta {
prefix: &'static str,
range: &'static str,
is_enum: bool,
}
struct Tables {
schema: &'static Schema,
type_origins: HashMap<&'static str, &'static [&'static str]>,
attr_origins: HashMap<&'static str, &'static [&'static str]>,
type_prefix: HashMap<&'static str, &'static str>,
type_ns: HashMap<&'static str, &'static str>,
attrs: HashMap<&'static str, AttrMeta>,
self_defining: HashSet<&'static str>,
}
fn tables() -> &'static Tables {
static T: OnceLock<Tables> = OnceLock::new();
T.get_or_init(|| {
let schema = type_registry().schema("cgmes").expect("the CGMES family is registered");
let prefix_of: HashMap<&str, &'static str> =
schema.namespaces.iter().map(|(p, ns)| (*ns, *p)).collect();
let lookup = |ns: &str| prefix_of.get(ns).copied().unwrap_or(FALLBACK_PREFIX);
let mut attr_origins: HashMap<&'static str, Vec<&'static str>> = HashMap::new();
for a in schema.classes.iter().flat_map(|c| c.attrs).filter(|a| a.used) {
let merged = attr_origins.entry(a.id).or_default();
for o in a.origins {
if !merged.contains(o) {
merged.push(o);
}
}
}
attr_origins.retain(|_, o| !o.is_empty());
let attr_origins: HashMap<&'static str, &'static [&'static str]> = attr_origins
.into_iter()
.map(|(id, mut o)| {
o.sort_by(|a, b| (*a != "EQ", *a).cmp(&(*b != "EQ", *b)));
(id, &*Vec::leak(o))
})
.collect();
let mut attrs: HashMap<&'static str, AttrMeta> = HashMap::new();
for a in schema.classes.iter().flat_map(|c| c.attrs).filter(|a| !a.ns.is_empty()) {
attrs.entry(a.id).or_insert(AttrMeta {
prefix: lookup(a.ns),
range: a.value_ns,
is_enum: a.kind == AttrKind::Enum,
});
}
Tables {
schema,
type_origins: schema
.classes
.iter()
.filter(|c| !c.origins.is_empty())
.map(|c| (c.local, c.origins))
.collect(),
self_defining: schema
.profiles
.iter()
.map(|(code, _)| *code)
.filter(|code| !attr_origins.values().any(|o| o.first() == Some(code)))
.collect(),
attr_origins,
type_prefix: schema.classes.iter().map(|c| (c.local, lookup(c.ns))).collect(),
type_ns: schema.classes.iter().filter(|c| !c.ns.is_empty()).map(|c| (c.local, c.ns)).collect(),
attrs,
}
})
}
fn prefix_for_type(type_name: &str) -> &'static str {
tables().type_prefix.get(type_name).copied().unwrap_or(FALLBACK_PREFIX)
}
fn type_ns_iri(type_name: &str) -> Option<&'static str> {
tables().type_ns.get(type_name).copied()
}
fn attr_meta(key: &str, type_name: &str) -> Option<AttrMeta> {
let t = tables();
t.attrs
.get(key)
.or_else(|| t.attrs.get(format!("{type_name}.{key}").as_str()))
.copied()
}
pub fn dataset_to_json(ds: &CimDataset) -> serde_json::Value {
let mut map = serde_json::Map::new();
for (mrid, entry) in &ds.entries {
let mut obj = entry.to_json_value().as_object().cloned().unwrap_or_default();
obj.insert("_type".into(), entry.type_name().into());
map.insert(mrid.clone(), serde_json::Value::Object(obj));
}
serde_json::Value::Object(map)
}
pub fn dataset_from_json(json: &str) -> Result<CimDataset, Box<dyn Error>> {
let root: serde_json::Map<String, serde_json::Value> = serde_json::from_str(json)?;
let reg = type_registry();
let mut ds = CimDataset::new();
for (mrid, val) in root {
let type_name = val["_type"].as_str().unwrap_or("");
if let Some(class) = reg.by_type_name(type_name) {
ds.set(mrid, Element::from_json(class, reg, &val)?);
}
}
Ok(ds)
}
fn is_foreign_family(type_name: &str) -> bool {
type_name.contains(':')
}
pub fn dataset_to_xml(ds: &CimDataset) -> Result<String, Box<dyn Error>> {
let mut body = String::new();
let cgmes = tables().schema.namespaces;
let mut prefixes = crate::convert_nc::Prefixes::seeded(ds, cgmes);
let mut mrids: Vec<&str> = ds.entries.keys().map(String::as_str).collect();
mrids.sort();
for mrid in mrids {
let entry = &ds.entries[mrid];
let block = entry;
let type_name = block.type_name();
if is_foreign_family(type_name) {
continue;
}
let ns = match ds.type_ns.get(mrid) {
Some(written) => prefixes.of(written),
None => prefix_for_type(type_name).to_string(),
};
write!(body, " <{ns}:{type_name} {}>", about(mrid))?;
let mut fields: Vec<(&&'static str, &FieldValue)> = block.fields().iter().collect();
fields.sort_by_key(|(k, _)| **k);
let mut children = String::new();
for (key, val) in fields {
let written = ds.field_forms.get(&(type_name, *key)).and_then(|f| f.ns).map(|n| prefixes.of(n));
write_field_as(&mut children, key, val, type_name, "#", ds, written.as_deref())?;
}
if children.is_empty() {
write!(body, "/>\n")?;
} else {
write!(body, "{children}\n </{ns}:{type_name}>\n")?;
}
}
crate::convert_nc::write_as_read(&mut body, &mut prefixes, ds)?;
let mut out = String::new();
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
out.push_str("<rdf:RDF");
for (prefix, uri) in cgmes {
write!(out, " xmlns:{prefix}=\"{uri}\"")?;
}
let mut extra: Vec<(&String, &&str)> = prefixes
.bound
.iter()
.filter(|(ns, _)| !cgmes.iter().any(|(_, c)| c == *ns))
.map(|(ns, p)| (p, ns))
.collect();
extra.sort();
for (prefix, uri) in extra {
write!(out, " xmlns:{prefix}=\"{}\"", escape_attr(uri))?;
}
out.push_str(">\n");
out.push_str(&body);
out.push_str("</rdf:RDF>\n");
Ok(out)
}
pub fn dataset_to_xml_for_profile(
ds: &CimDataset,
profile_code: &str,
) -> Result<String, Box<dyn Error>> {
let tables = tables();
if !tables.schema.profiles.iter().any(|(k, _)| *k == profile_code) {
if crate::convert_nc::nc_profile_uri(profile_code).is_some() {
return crate::convert_nc::dataset_to_xml_for_nc_profile(ds, profile_code);
}
return Err(format!("unknown profile code {profile_code:?}: neither a CGMES nor an NC profile").into());
}
let type_map = &tables.type_origins;
let attr_map = &tables.attr_origins;
let self_defining = tables.self_defining.contains(profile_code);
let mut out = String::new();
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
out.push_str("<rdf:RDF");
for (prefix, uri) in tables.schema.namespaces {
write!(out, " xmlns:{prefix}=\"{uri}\"")?;
}
out.push_str(">\n");
if let Some(&(_, uri)) = tables.schema.profiles.iter().find(|(k, _)| *k == profile_code) {
let hdr = prefix_for_type("FullModel");
if let Some((mrid, block)) = find_full_model_header(ds, uri) {
write!(out, " <{hdr}:FullModel rdf:about=\"{}\">", escape_attr(mrid))?;
let mut fields: Vec<(&&'static str, &FieldValue)> = block.fields().iter().collect();
fields.sort_by_key(|(k, _)| **k);
let mut children = String::new();
for (key, val) in fields {
write_field(&mut children, key, val, "FullModel", "", ds)?;
}
if children.is_empty() {
write!(out, "/>\n")?;
} else {
write!(out, "{children}\n </{hdr}:FullModel>\n")?;
}
} else {
write_synthesized_header(&mut out, ds, profile_code, uri)?;
}
}
let mut mrids: Vec<&str> = ds.entries.keys().map(String::as_str).collect();
mrids.sort();
for mrid in mrids {
let entry = &ds.entries[mrid];
let type_name = entry.type_name();
if is_foreign_family(type_name) {
continue;
}
let type_origins: &[&str] = type_map.get(type_name).copied().unwrap_or(&[]);
if !type_origins.contains(&profile_code) {
write_carried_fields(&mut out, mrid, entry, profile_code, type_map, attr_map, ds)?;
continue;
}
let is_primary =
self_defining || type_origins.first().map_or(false, |&o| o == profile_code);
let block = entry;
let include_field = |key: &str| -> bool {
let origins = attr_map.get(key).copied().unwrap_or(&[]);
if is_primary {
origins.contains(&profile_code)
} else {
origins.first().map_or(false, |&o| o == profile_code)
}
};
let mut fields: Vec<(&&'static str, &FieldValue)> =
block.fields().iter().filter(|(k, _)| include_field(k)).collect();
if fields.is_empty() {
continue;
}
let ns = prefix_for_type(type_name);
if is_primary {
if mrid.contains(':') {
write!(out, " <{ns}:{type_name} {}>", about(mrid))?;
} else {
write!(out, " <{ns}:{type_name} rdf:ID=\"{}\">", escape_attr(mrid))?;
}
} else {
write!(out, " <{ns}:{type_name} {}>", about(mrid))?;
}
fields.sort_by_key(|(k, _)| **k);
let mut children = String::new();
for (key, val) in fields {
write_field(&mut children, key, val, type_name, "#", ds)?;
}
if children.is_empty() {
write!(out, "/>\n")?;
} else {
write!(out, "{children}\n </{ns}:{type_name}>\n")?;
}
}
out.push_str("</rdf:RDF>\n");
Ok(out)
}
const HEADER_UUID_NAMESPACE: [u8; 16] = [
0x3b, 0x8e, 0x5f, 0x0a, 0x9c, 0x41, 0x4d, 0x2e, 0xa7, 0x16, 0x5d, 0x0b, 0x6f, 0x93, 0xc2, 0x48,
];
const UNKNOWN_TIME: &str = "1970-01-01T00:00:00Z";
const UNKNOWN_AUTHORITY: &str = "urn:cimoxide:unknown-modeling-authority-set";
fn write_synthesized_header(
out: &mut String,
ds: &CimDataset,
profile_code: &str,
profile_uri: &str,
) -> Result<(), Box<dyn Error>> {
let hdr = prefix_for_type("FullModel");
let md = |field: &str| {
attr_meta(&format!("Model.{field}"), "FullModel").map_or(FALLBACK_PREFIX, |m| m.prefix)
};
let inherited = |field: &str| -> Option<String> {
let mut headers: Vec<(&String, &Element)> = ds
.entries
.iter()
.filter(|(_, e)| e.type_name() == "FullModel")
.collect();
headers.sort_by_key(|(m, _)| m.as_str());
headers.into_iter().find_map(|(_, b)| match b.fields().get(format!("Model.{field}").as_str()) {
Some(FieldValue::Text(v)) if !v.is_empty() => Some(v.clone()),
_ => None,
})
};
let scenario_time = inherited("scenarioTime").unwrap_or_else(|| UNKNOWN_TIME.to_string());
let created = inherited("created").unwrap_or_else(|| scenario_time.clone());
let authority = inherited("modelingAuthoritySet").unwrap_or_else(|| UNKNOWN_AUTHORITY.to_string());
writeln!(out, " <{hdr}:FullModel rdf:about=\"{}\">", header_urn(ds, profile_code))?;
let text = |out: &mut String, field: &str, value: &str| -> std::fmt::Result {
let p = md(field);
writeln!(out, " <{p}:Model.{field}>{}</{p}:Model.{field}>", escape_text(value))
};
text(out, "created", &created)?;
text(out, "description", &format!("{profile_code} profile, header synthesized by cimoxide"))?;
text(out, "modelingAuthoritySet", &authority)?;
text(out, "profile", profile_uri)?;
text(out, "scenarioTime", &scenario_time)?;
text(out, "version", "1")?;
writeln!(out, " </{hdr}:FullModel>")?;
Ok(())
}
pub(crate) fn header_urn(ds: &CimDataset, profile_code: &str) -> String {
use sha1::{Digest, Sha1};
let mut mrids: Vec<&str> = ds.entries.keys().map(String::as_str).collect();
mrids.sort_unstable();
let mut h = Sha1::new();
h.update(HEADER_UUID_NAMESPACE);
h.update(profile_code.as_bytes());
for m in mrids {
h.update([0u8]);
h.update(m.as_bytes());
}
let d = h.finalize();
let mut b = [0u8; 16];
b.copy_from_slice(&d[..16]);
b[6] = (b[6] & 0x0f) | 0x50; b[8] = (b[8] & 0x3f) | 0x80; let hex: String = b.iter().map(|x| format!("{x:02x}")).collect();
format!("urn:uuid:{}-{}-{}-{}-{}", &hex[0..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..32])
}
fn find_full_model_header<'a>(ds: &'a CimDataset, profile_uri: &str) -> Option<(&'a str, &'a Element)> {
let mut best: Option<(&str, &Element)> = None;
for (mrid, entry) in &ds.entries {
if entry.type_name() != "FullModel" {
continue;
}
let matches = match entry.fields().get("Model.profile") {
Some(FieldValue::Text(s)) => s == profile_uri,
Some(FieldValue::TextList(list)) => list.iter().any(|s| s == profile_uri),
_ => false,
};
if !matches {
continue;
}
if best.is_none_or(|(m, _)| mrid.as_str() < m) {
best = Some((mrid.as_str(), entry));
}
}
best
}
fn write_carried_fields(
out: &mut String,
mrid: &str,
block: &Element,
profile_code: &str,
type_map: &HashMap<&'static str, &'static [&'static str]>,
attr_map: &HashMap<&'static str, &'static [&'static str]>,
ds: &CimDataset,
) -> Result<(), Box<dyn Error>> {
let mut by_class: std::collections::BTreeMap<&str, Vec<(&&'static str, &FieldValue)>> =
std::collections::BTreeMap::new();
for (key, val) in block.fields() {
let only_here = attr_map.get(*key).is_some_and(|o| *o == [profile_code]);
let Some((class, _)) = key.split_once('.') else { continue };
let class_in_profile = type_map.get(class).is_some_and(|o| o.contains(&profile_code));
if only_here && class_in_profile {
by_class.entry(class).or_default().push((key, val));
}
}
for (class, mut fields) in by_class {
let ns = prefix_for_type(class);
write!(out, " <{ns}:{class} {}>", about(mrid))?;
fields.sort_by_key(|(k, _)| **k);
let mut children = String::new();
for (key, val) in fields {
write_field(&mut children, key, val, class, "#", ds)?;
}
write!(out, "{children}\n </{ns}:{class}>\n")?;
}
Ok(())
}
fn write_field(
children: &mut String,
key: &str,
val: &FieldValue,
type_name: &str,
resource_prefix: &str,
ds: &CimDataset,
) -> Result<(), Box<dyn Error>> {
write_field_as(children, key, val, type_name, resource_prefix, ds, None)
}
fn write_field_as(
children: &mut String,
key: &str,
val: &FieldValue,
type_name: &str,
resource_prefix: &str,
ds: &CimDataset,
prefix: Option<&str>,
) -> Result<(), Box<dyn Error>> {
let meta = attr_meta(key, type_name);
let ns = prefix.unwrap_or_else(|| meta.map_or(FALLBACK_PREFIX, |m| m.prefix));
let resource = |r: &str| {
if let Some(m) = meta
&& m.is_enum
{
return format!("{}{}", m.range, escape_attr(r));
}
if !ds.entries.contains_key(r)
&& let Some((owner, _)) = r.split_once('.')
&& let Some(ns_iri) = type_ns_iri(owner)
{
return format!("{ns_iri}{}", escape_attr(r));
}
if r.contains(':') {
return escape_attr(r);
}
format!("{resource_prefix}{}", escape_attr(r))
};
match val {
FieldValue::Text(s) => {
write!(children, "\n <{ns}:{key}>{}</{ns}:{key}>", escape_text(s))?;
}
FieldValue::TextList(ss) => {
for s in ss {
write!(children, "\n <{ns}:{key}>{}</{ns}:{key}>", escape_text(s))?;
}
}
FieldValue::Resource(r) => {
write!(children, "\n <{ns}:{key} rdf:resource=\"{}\"/>", resource(r))?;
}
FieldValue::ResourceList(rs) => {
for r in rs {
write!(children, "\n <{ns}:{key} rdf:resource=\"{}\"/>", resource(r))?;
}
}
}
Ok(())
}
fn about(mrid: &str) -> String {
if mrid.contains(':') {
format!("rdf:about=\"{}\"", escape_attr(mrid))
} else {
format!("rdf:about=\"#{}\"", escape_attr(mrid))
}
}
fn escape_text(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
fn escape_attr(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('"', """)
}