1use std::collections::{HashMap, HashSet};
2use std::error::Error;
3use std::fmt::Write as FmtWrite;
4use std::sync::OnceLock;
5
6use crate::CimDataset;
7use crate::base::{Element, FieldValue};
8use crate::base::{AttrKind, Schema};
9use crate::registry::type_registry;
10
11const FALLBACK_PREFIX: &str = "cim";
14
15#[derive(Clone, Copy)]
17struct AttrMeta {
18 prefix: &'static str,
19 range: &'static str,
21 is_enum: bool,
22}
23
24struct Tables {
27 schema: &'static Schema,
28 type_origins: HashMap<&'static str, &'static [&'static str]>,
29 attr_origins: HashMap<&'static str, &'static [&'static str]>,
30 type_prefix: HashMap<&'static str, &'static str>,
31 type_ns: HashMap<&'static str, &'static str>,
32 attrs: HashMap<&'static str, AttrMeta>,
33 self_defining: HashSet<&'static str>,
36}
37
38fn tables() -> &'static Tables {
39 static T: OnceLock<Tables> = OnceLock::new();
40 T.get_or_init(|| {
41 let schema = type_registry().schema("cgmes").expect("the CGMES family is registered");
42 let prefix_of: HashMap<&str, &'static str> =
46 schema.namespaces.iter().map(|(p, ns)| (*ns, *p)).collect();
47 let lookup = |ns: &str| prefix_of.get(ns).copied().unwrap_or(FALLBACK_PREFIX);
48
49 let mut attr_origins: HashMap<&'static str, Vec<&'static str>> = HashMap::new();
52 for a in schema.classes.iter().flat_map(|c| c.attrs).filter(|a| a.used) {
53 let merged = attr_origins.entry(a.id).or_default();
54 for o in a.origins {
55 if !merged.contains(o) {
56 merged.push(o);
57 }
58 }
59 }
60 attr_origins.retain(|_, o| !o.is_empty());
61 let attr_origins: HashMap<&'static str, &'static [&'static str]> = attr_origins
62 .into_iter()
63 .map(|(id, mut o)| {
64 o.sort_by(|a, b| (*a != "EQ", *a).cmp(&(*b != "EQ", *b)));
65 (id, &*Vec::leak(o))
66 })
67 .collect();
68
69 let mut attrs: HashMap<&'static str, AttrMeta> = HashMap::new();
70 for a in schema.classes.iter().flat_map(|c| c.attrs).filter(|a| !a.ns.is_empty()) {
71 attrs.entry(a.id).or_insert(AttrMeta {
72 prefix: lookup(a.ns),
73 range: a.value_ns,
74 is_enum: a.kind == AttrKind::Enum,
75 });
76 }
77
78 Tables {
79 schema,
80 type_origins: schema
81 .classes
82 .iter()
83 .filter(|c| !c.origins.is_empty())
84 .map(|c| (c.local, c.origins))
85 .collect(),
86 self_defining: schema
87 .profiles
88 .iter()
89 .map(|(code, _)| *code)
90 .filter(|code| !attr_origins.values().any(|o| o.first() == Some(code)))
91 .collect(),
92 attr_origins,
93 type_prefix: schema.classes.iter().map(|c| (c.local, lookup(c.ns))).collect(),
94 type_ns: schema.classes.iter().filter(|c| !c.ns.is_empty()).map(|c| (c.local, c.ns)).collect(),
95 attrs,
96 }
97 })
98}
99
100fn prefix_for_type(type_name: &str) -> &'static str {
102 tables().type_prefix.get(type_name).copied().unwrap_or(FALLBACK_PREFIX)
103}
104
105fn type_ns_iri(type_name: &str) -> Option<&'static str> {
107 tables().type_ns.get(type_name).copied()
108}
109
110fn attr_meta(key: &str, type_name: &str) -> Option<AttrMeta> {
116 let t = tables();
117 t.attrs
118 .get(key)
119 .or_else(|| t.attrs.get(format!("{type_name}.{key}").as_str()))
120 .copied()
121}
122
123pub fn dataset_to_json(ds: &CimDataset) -> serde_json::Value {
124 let mut map = serde_json::Map::new();
125 for (mrid, entry) in &ds.entries {
126 let mut obj = entry.to_json_value().as_object().cloned().unwrap_or_default();
127 obj.insert("_type".into(), entry.type_name().into());
128 map.insert(mrid.clone(), serde_json::Value::Object(obj));
129 }
130 serde_json::Value::Object(map)
131}
132
133pub fn dataset_from_json(json: &str) -> Result<CimDataset, Box<dyn Error>> {
134 let root: serde_json::Map<String, serde_json::Value> = serde_json::from_str(json)?;
135 let reg = type_registry();
136 let mut ds = CimDataset::new();
137 for (mrid, val) in root {
138 let type_name = val["_type"].as_str().unwrap_or("");
139 if let Some(class) = reg.by_type_name(type_name) {
140 ds.set(mrid, Element::from_json(class, reg, &val)?);
141 }
142 }
143 Ok(ds)
144}
145
146fn is_foreign_family(type_name: &str) -> bool {
150 type_name.contains(':')
151}
152
153pub fn dataset_to_xml(ds: &CimDataset) -> Result<String, Box<dyn Error>> {
158 let mut body = String::new();
159 let cgmes = tables().schema.namespaces;
160 let mut prefixes = crate::convert_nc::Prefixes::seeded(ds, cgmes);
161
162 let mut mrids: Vec<&str> = ds.entries.keys().map(String::as_str).collect();
163 mrids.sort();
164
165 for mrid in mrids {
166 let entry = &ds.entries[mrid];
167 let block = entry;
168 let type_name = block.type_name();
169 if is_foreign_family(type_name) {
170 continue;
171 }
172 let ns = match ds.type_ns.get(mrid) {
174 Some(written) => prefixes.of(written),
175 None => prefix_for_type(type_name).to_string(),
176 };
177 write!(body, " <{ns}:{type_name} {}>", about(mrid))?;
178
179 let mut fields: Vec<(&&'static str, &FieldValue)> = block.fields().iter().collect();
181 fields.sort_by_key(|(k, _)| **k);
182
183 let mut children = String::new();
184 for (key, val) in fields {
185 let written = ds.field_forms.get(&(type_name, *key)).and_then(|f| f.ns).map(|n| prefixes.of(n));
186 write_field_as(&mut children, key, val, type_name, "#", ds, written.as_deref())?;
187 }
188
189 if children.is_empty() {
190 write!(body, "/>\n")?;
191 } else {
192 write!(body, "{children}\n </{ns}:{type_name}>\n")?;
193 }
194 }
195
196 crate::convert_nc::write_as_read(&mut body, &mut prefixes, ds)?;
197
198 let mut out = String::new();
199 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
200 out.push_str("<rdf:RDF");
201 for (prefix, uri) in cgmes {
202 write!(out, " xmlns:{prefix}=\"{uri}\"")?;
203 }
204 let mut extra: Vec<(&String, &&str)> = prefixes
205 .bound
206 .iter()
207 .filter(|(ns, _)| !cgmes.iter().any(|(_, c)| c == *ns))
208 .map(|(ns, p)| (p, ns))
209 .collect();
210 extra.sort();
211 for (prefix, uri) in extra {
212 write!(out, " xmlns:{prefix}=\"{}\"", escape_attr(uri))?;
213 }
214 out.push_str(">\n");
215 out.push_str(&body);
216 out.push_str("</rdf:RDF>\n");
217 Ok(out)
218}
219
220pub fn dataset_to_xml_for_profile(
226 ds: &CimDataset,
227 profile_code: &str,
228) -> Result<String, Box<dyn Error>> {
229 let tables = tables();
230 if !tables.schema.profiles.iter().any(|(k, _)| *k == profile_code) {
231 if crate::convert_nc::nc_profile_uri(profile_code).is_some() {
232 return crate::convert_nc::dataset_to_xml_for_nc_profile(ds, profile_code);
233 }
234 return Err(format!("unknown profile code {profile_code:?}: neither a CGMES nor an NC profile").into());
235 }
236 let type_map = &tables.type_origins;
237 let attr_map = &tables.attr_origins;
238
239 let self_defining = tables.self_defining.contains(profile_code);
247
248 let mut out = String::new();
249 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
250 out.push_str("<rdf:RDF");
251 for (prefix, uri) in tables.schema.namespaces {
252 write!(out, " xmlns:{prefix}=\"{uri}\"")?;
253 }
254 out.push_str(">\n");
255
256 if let Some(&(_, uri)) = tables.schema.profiles.iter().find(|(k, _)| *k == profile_code) {
261 let hdr = prefix_for_type("FullModel");
266 if let Some((mrid, block)) = find_full_model_header(ds, uri) {
267 write!(out, " <{hdr}:FullModel rdf:about=\"{}\">", escape_attr(mrid))?;
268
269 let mut fields: Vec<(&&'static str, &FieldValue)> = block.fields().iter().collect();
270 fields.sort_by_key(|(k, _)| **k);
271
272 let mut children = String::new();
273 for (key, val) in fields {
274 write_field(&mut children, key, val, "FullModel", "", ds)?;
275 }
276
277 if children.is_empty() {
278 write!(out, "/>\n")?;
279 } else {
280 write!(out, "{children}\n </{hdr}:FullModel>\n")?;
281 }
282 } else {
283 write_synthesized_header(&mut out, ds, profile_code, uri)?;
284 }
285 }
286
287 let mut mrids: Vec<&str> = ds.entries.keys().map(String::as_str).collect();
288 mrids.sort();
289
290 for mrid in mrids {
291 let entry = &ds.entries[mrid];
292 let type_name = entry.type_name();
293
294 if is_foreign_family(type_name) {
298 continue;
299 }
300
301 let type_origins: &[&str] = type_map.get(type_name).copied().unwrap_or(&[]);
302 if !type_origins.contains(&profile_code) {
303 write_carried_fields(&mut out, mrid, entry, profile_code, type_map, attr_map, ds)?;
304 continue;
305 }
306
307 let is_primary =
308 self_defining || type_origins.first().map_or(false, |&o| o == profile_code);
309 let block = entry;
310
311 let include_field = |key: &str| -> bool {
312 let origins = attr_map.get(key).copied().unwrap_or(&[]);
313 if is_primary {
314 origins.contains(&profile_code)
315 } else {
316 origins.first().map_or(false, |&o| o == profile_code)
317 }
318 };
319
320 let mut fields: Vec<(&&'static str, &FieldValue)> =
321 block.fields().iter().filter(|(k, _)| include_field(k)).collect();
322
323 if fields.is_empty() {
324 continue;
325 }
326
327 let ns = prefix_for_type(type_name);
328 if is_primary {
329 if mrid.contains(':') {
330 write!(out, " <{ns}:{type_name} {}>", about(mrid))?;
331 } else {
332 write!(out, " <{ns}:{type_name} rdf:ID=\"{}\">", escape_attr(mrid))?;
333 }
334 } else {
335 write!(out, " <{ns}:{type_name} {}>", about(mrid))?;
336 }
337
338 fields.sort_by_key(|(k, _)| **k);
339 let mut children = String::new();
340 for (key, val) in fields {
341 write_field(&mut children, key, val, type_name, "#", ds)?;
342 }
343
344 if children.is_empty() {
345 write!(out, "/>\n")?;
346 } else {
347 write!(out, "{children}\n </{ns}:{type_name}>\n")?;
348 }
349 }
350
351 out.push_str("</rdf:RDF>\n");
352 Ok(out)
353}
354
355const HEADER_UUID_NAMESPACE: [u8; 16] = [
357 0x3b, 0x8e, 0x5f, 0x0a, 0x9c, 0x41, 0x4d, 0x2e, 0xa7, 0x16, 0x5d, 0x0b, 0x6f, 0x93, 0xc2, 0x48,
358];
359const UNKNOWN_TIME: &str = "1970-01-01T00:00:00Z";
361const UNKNOWN_AUTHORITY: &str = "urn:cimoxide:unknown-modeling-authority-set";
362
363fn write_synthesized_header(
379 out: &mut String,
380 ds: &CimDataset,
381 profile_code: &str,
382 profile_uri: &str,
383) -> Result<(), Box<dyn Error>> {
384 let hdr = prefix_for_type("FullModel");
385 let md = |field: &str| {
386 attr_meta(&format!("Model.{field}"), "FullModel").map_or(FALLBACK_PREFIX, |m| m.prefix)
387 };
388 let inherited = |field: &str| -> Option<String> {
389 let mut headers: Vec<(&String, &Element)> = ds
390 .entries
391 .iter()
392 .filter(|(_, e)| e.type_name() == "FullModel")
393 .collect();
394 headers.sort_by_key(|(m, _)| m.as_str());
395 headers.into_iter().find_map(|(_, b)| match b.fields().get(format!("Model.{field}").as_str()) {
396 Some(FieldValue::Text(v)) if !v.is_empty() => Some(v.clone()),
397 _ => None,
398 })
399 };
400 let scenario_time = inherited("scenarioTime").unwrap_or_else(|| UNKNOWN_TIME.to_string());
401 let created = inherited("created").unwrap_or_else(|| scenario_time.clone());
402 let authority = inherited("modelingAuthoritySet").unwrap_or_else(|| UNKNOWN_AUTHORITY.to_string());
403
404 writeln!(out, " <{hdr}:FullModel rdf:about=\"{}\">", header_urn(ds, profile_code))?;
405 let text = |out: &mut String, field: &str, value: &str| -> std::fmt::Result {
406 let p = md(field);
407 writeln!(out, " <{p}:Model.{field}>{}</{p}:Model.{field}>", escape_text(value))
408 };
409 text(out, "created", &created)?;
410 text(out, "description", &format!("{profile_code} profile, header synthesized by cimoxide"))?;
411 text(out, "modelingAuthoritySet", &authority)?;
412 text(out, "profile", profile_uri)?;
413 text(out, "scenarioTime", &scenario_time)?;
414 text(out, "version", "1")?;
415 writeln!(out, " </{hdr}:FullModel>")?;
416 Ok(())
417}
418
419pub(crate) fn header_urn(ds: &CimDataset, profile_code: &str) -> String {
422 use sha1::{Digest, Sha1};
423 let mut mrids: Vec<&str> = ds.entries.keys().map(String::as_str).collect();
424 mrids.sort_unstable();
425 let mut h = Sha1::new();
426 h.update(HEADER_UUID_NAMESPACE);
427 h.update(profile_code.as_bytes());
428 for m in mrids {
429 h.update([0u8]);
430 h.update(m.as_bytes());
431 }
432 let d = h.finalize();
433 let mut b = [0u8; 16];
434 b.copy_from_slice(&d[..16]);
435 b[6] = (b[6] & 0x0f) | 0x50; b[8] = (b[8] & 0x3f) | 0x80; let hex: String = b.iter().map(|x| format!("{x:02x}")).collect();
438 format!("urn:uuid:{}-{}-{}-{}-{}", &hex[0..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..32])
439}
440
441fn find_full_model_header<'a>(ds: &'a CimDataset, profile_uri: &str) -> Option<(&'a str, &'a Element)> {
445 let mut best: Option<(&str, &Element)> = None;
446 for (mrid, entry) in &ds.entries {
447 if entry.type_name() != "FullModel" {
448 continue;
449 }
450 let matches = match entry.fields().get("Model.profile") {
451 Some(FieldValue::Text(s)) => s == profile_uri,
452 Some(FieldValue::TextList(list)) => list.iter().any(|s| s == profile_uri),
453 _ => false,
454 };
455 if !matches {
456 continue;
457 }
458 if best.is_none_or(|(m, _)| mrid.as_str() < m) {
459 best = Some((mrid.as_str(), entry));
460 }
461 }
462 best
463}
464
465fn write_carried_fields(
481 out: &mut String,
482 mrid: &str,
483 block: &Element,
484 profile_code: &str,
485 type_map: &HashMap<&'static str, &'static [&'static str]>,
486 attr_map: &HashMap<&'static str, &'static [&'static str]>,
487 ds: &CimDataset,
488) -> Result<(), Box<dyn Error>> {
489 let mut by_class: std::collections::BTreeMap<&str, Vec<(&&'static str, &FieldValue)>> =
490 std::collections::BTreeMap::new();
491 for (key, val) in block.fields() {
492 let only_here = attr_map.get(*key).is_some_and(|o| *o == [profile_code]);
493 let Some((class, _)) = key.split_once('.') else { continue };
494 let class_in_profile = type_map.get(class).is_some_and(|o| o.contains(&profile_code));
495 if only_here && class_in_profile {
496 by_class.entry(class).or_default().push((key, val));
497 }
498 }
499 for (class, mut fields) in by_class {
500 let ns = prefix_for_type(class);
501 write!(out, " <{ns}:{class} {}>", about(mrid))?;
502 fields.sort_by_key(|(k, _)| **k);
503 let mut children = String::new();
504 for (key, val) in fields {
505 write_field(&mut children, key, val, class, "#", ds)?;
506 }
507 write!(out, "{children}\n </{ns}:{class}>\n")?;
508 }
509 Ok(())
510}
511
512fn write_field(
526 children: &mut String,
527 key: &str,
528 val: &FieldValue,
529 type_name: &str,
530 resource_prefix: &str,
531 ds: &CimDataset,
532) -> Result<(), Box<dyn Error>> {
533 write_field_as(children, key, val, type_name, resource_prefix, ds, None)
534}
535
536fn write_field_as(
538 children: &mut String,
539 key: &str,
540 val: &FieldValue,
541 type_name: &str,
542 resource_prefix: &str,
543 ds: &CimDataset,
544 prefix: Option<&str>,
545) -> Result<(), Box<dyn Error>> {
546 let meta = attr_meta(key, type_name);
547 let ns = prefix.unwrap_or_else(|| meta.map_or(FALLBACK_PREFIX, |m| m.prefix));
548 let resource = |r: &str| {
549 if let Some(m) = meta
550 && m.is_enum
551 {
552 return format!("{}{}", m.range, escape_attr(r));
553 }
554 if !ds.entries.contains_key(r)
560 && let Some((owner, _)) = r.split_once('.')
561 && let Some(ns_iri) = type_ns_iri(owner)
562 {
563 return format!("{ns_iri}{}", escape_attr(r));
564 }
565 if r.contains(':') {
567 return escape_attr(r);
568 }
569 format!("{resource_prefix}{}", escape_attr(r))
570 };
571
572 match val {
573 FieldValue::Text(s) => {
574 write!(children, "\n <{ns}:{key}>{}</{ns}:{key}>", escape_text(s))?;
575 }
576 FieldValue::TextList(ss) => {
577 for s in ss {
578 write!(children, "\n <{ns}:{key}>{}</{ns}:{key}>", escape_text(s))?;
579 }
580 }
581 FieldValue::Resource(r) => {
582 write!(children, "\n <{ns}:{key} rdf:resource=\"{}\"/>", resource(r))?;
583 }
584 FieldValue::ResourceList(rs) => {
585 for r in rs {
586 write!(children, "\n <{ns}:{key} rdf:resource=\"{}\"/>", resource(r))?;
587 }
588 }
589 }
590 Ok(())
591}
592
593fn about(mrid: &str) -> String {
596 if mrid.contains(':') {
597 format!("rdf:about=\"{}\"", escape_attr(mrid))
598 } else {
599 format!("rdf:about=\"#{}\"", escape_attr(mrid))
600 }
601}
602
603fn escape_text(s: &str) -> String {
604 s.replace('&', "&")
605 .replace('<', "<")
606 .replace('>', ">")
607}
608
609fn escape_attr(s: &str) -> String {
610 s.replace('&', "&")
611 .replace('<', "<")
612 .replace('"', """)
613}