Skip to main content

cimmodel/
base.rs

1use std::collections::{HashMap, HashSet};
2use std::hash::{BuildHasherDefault, Hasher};
3
4// --- Hashing -----------------------------------------------------------------
5
6/// A fast, non-cryptographic hasher for the decoder's maps.
7///
8/// Every field read during validation and every element lookup goes through a
9/// `String`-keyed map, and std's SipHash made those lookups a measurable share
10/// of a validation run. This is the multiply-rotate scheme of FxHash (as in
11/// `rustc-hash` 2): a word at a time, with the high bits rotated down at the
12/// end because hashbrown takes its bucket index from the low bits.
13///
14/// Written out rather than imported so the core crates carry no dependency for
15/// it. Not resistant to deliberately colliding keys, which is acceptable for a
16/// tool that reads grid model files rather than serving untrusted requests.
17#[derive(Default, Clone, Copy)]
18pub struct FastHasher {
19    hash: u64,
20}
21
22const SEED: u64 = 0xf135_7aea_2e62_a9c5;
23
24impl FastHasher {
25    #[inline]
26    fn add(&mut self, word: u64) {
27        self.hash = self.hash.wrapping_add(word).wrapping_mul(SEED);
28    }
29}
30
31impl Hasher for FastHasher {
32    #[inline]
33    fn write(&mut self, bytes: &[u8]) {
34        let mut chunks = bytes.chunks_exact(8);
35        for c in &mut chunks {
36            self.add(u64::from_le_bytes(c.try_into().expect("chunk of 8")));
37        }
38        let rest = chunks.remainder();
39        if !rest.is_empty() {
40            let mut buf = [0u8; 8];
41            buf[..rest.len()].copy_from_slice(rest);
42            self.add(u64::from_le_bytes(buf));
43        }
44    }
45    #[inline]
46    fn write_u8(&mut self, i: u8) {
47        self.add(i as u64);
48    }
49    #[inline]
50    fn write_u32(&mut self, i: u32) {
51        self.add(i as u64);
52    }
53    #[inline]
54    fn write_u64(&mut self, i: u64) {
55        self.add(i);
56    }
57    #[inline]
58    fn write_usize(&mut self, i: usize) {
59        self.add(i as u64);
60    }
61    #[inline]
62    fn finish(&self) -> u64 {
63        self.hash.rotate_left(26)
64    }
65}
66
67pub type FastBuildHasher = BuildHasherDefault<FastHasher>;
68pub type FastMap<K, V> = HashMap<K, V, FastBuildHasher>;
69pub type FastSet<T> = HashSet<T, FastBuildHasher>;
70/// An element's fields, keyed by `Class.attr`. Keys are `&'static`: a declared
71/// attribute's key is its id in the class table, any other goes through
72/// [`intern`], so a key is allocated once per process rather than per field.
73pub type FieldMap = FastMap<&'static str, FieldValue>;
74
75/// The one `&'static` copy of a field key the class tables do not hold — an
76/// attribute the element's class does not declare, as written in the file or
77/// set from JSON. Each distinct key is leaked once and kept for the process;
78/// there are as many as distinct undeclared attribute names, not fields.
79pub fn intern(key: &str) -> &'static str {
80    use std::cell::RefCell;
81    use std::sync::{Mutex, OnceLock};
82    thread_local! {
83        static LOCAL: RefCell<FastSet<&'static str>> = RefCell::new(FastSet::default());
84    }
85    static GLOBAL: OnceLock<Mutex<FastSet<&'static str>>> = OnceLock::new();
86    if let Some(k) = LOCAL.with(|l| l.borrow().get(key).copied()) {
87        return k;
88    }
89    let k = {
90        let mut global = GLOBAL.get_or_init(Default::default).lock().unwrap();
91        match global.get(key) {
92            Some(k) => *k,
93            None => {
94                let k: &'static str = Box::leak(key.to_owned().into_boxed_str());
95                global.insert(k);
96                k
97            }
98        }
99    };
100    LOCAL.with(|l| l.borrow_mut().insert(k));
101    k
102}
103
104/// The class of an object no class table knows — a SKOS concept, a PROV
105/// agent, a DCAT catalogue — named by its namespace and local name. One leaked
106/// `ClassDef` per distinct class, kept for the process like [`intern`]'s keys.
107/// Its `qualified` name is the class IRI, which no family's `by_type` keys use.
108pub fn foreign_class(ns: &str, local: &str) -> &'static ClassDef {
109    use std::sync::{Mutex, OnceLock};
110    static CLASSES: OnceLock<Mutex<FastMap<String, &'static ClassDef>>> = OnceLock::new();
111    let iri = format!("{ns}{local}");
112    let mut classes = CLASSES.get_or_init(Default::default).lock().unwrap();
113    if let Some(c) = classes.get(&iri) {
114        return c;
115    }
116    let qualified: &'static str = Box::leak(iri.clone().into_boxed_str());
117    let class: &'static ClassDef = Box::leak(Box::new(ClassDef {
118        ns: intern(ns),
119        local: &qualified[ns.len()..],
120        qualified,
121        super_class: None,
122        concrete: true,
123        attrs: &[],
124        origins: &[],
125        comment: "",
126    }));
127    classes.insert(iri, class);
128    class
129}
130
131#[derive(Debug, Clone)]
132pub enum FieldValue {
133    Text(String),
134    /// Repeated text element within one parsed element (e.g. the multiple
135    /// `md:Model.profile` entries of a combined EQ+SC file header).
136    TextList(Vec<String>),
137    Resource(String),
138    ResourceList(Vec<String>),
139}
140
141// --- Elements ----------------------------------------------------------------
142
143/// One decoded CIM object: its class, its mRID, and its fields as written.
144///
145/// Every family decodes into this one type. The fields are the document's
146/// own, keyed by `Class.attr` (the XML element's local name), with values
147/// unparsed: what a class declares lives in its [`ClassDef`], not in a
148/// generated type, so an attribute the class does not declare and a value
149/// that does not parse are kept rather than dropped.
150#[derive(Debug, Clone)]
151pub struct Element {
152    class: &'static ClassDef,
153    mrid: String,
154    fields: FieldMap,
155    /// Fields assigned more than once within the element, which `sh:maxCount`
156    /// reads.
157    duplicate_fields: FastSet<&'static str>,
158    /// The `xml:lang` of each text value written with one, by field, in the
159    /// order read. `None` until a value carries one, so an element without
160    /// language tags — nearly all of them — pays a pointer.
161    langs: Option<Box<Langs>>,
162    /// The other classes files wrote the object under, when merged datasets
163    /// disagree — an SSH file's `cim:Equipment` for an EQ file's
164    /// `cim:ACLineSegment`. In RDF the object has every one of those types;
165    /// `class` is the most specific. `None` for nearly every element, so it
166    /// costs a pointer, like `langs`.
167    also: Option<Box<Types>>,
168    /// Every file that wrote the object only described it (`rdf:about`) and
169    /// none defined it (`rdf:ID`): an SSH file's `inService` for equipment
170    /// the EQ file defines. Elements not decoded from RDF/XML are defined.
171    described: bool,
172}
173
174/// `(field key, xml:lang)` for each tagged text value of an element.
175type Langs = Vec<(&'static str, Box<str>)>;
176
177/// The classes besides its own a merged element was written under.
178type Types = Vec<&'static ClassDef>;
179
180impl Element {
181    pub fn new(class: &'static ClassDef, mrid: String) -> Self {
182        Self { class, mrid, fields: FieldMap::default(), duplicate_fields: FastSet::default(), langs: None, also: None, described: false }
183    }
184
185    pub fn with_fields(class: &'static ClassDef, mrid: String, fields: FieldMap) -> Self {
186        Self { class, mrid, fields, duplicate_fields: FastSet::default(), langs: None, also: None, described: false }
187    }
188
189    pub fn class(&self) -> &'static ClassDef {
190        self.class
191    }
192
193    /// Every class the object is typed as: its class, then any other a merged
194    /// file wrote it under ([`CimDataset::merge`](crate::CimDataset::merge)).
195    pub fn types(&self) -> impl Iterator<Item = &'static ClassDef> + '_ {
196        std::iter::once(self.class).chain(self.also.iter().flat_map(|v| v.iter().copied()))
197    }
198
199    /// Record that a file also typed the object as `other`. Becomes its class
200    /// when it is a subclass of the current one (field keys name each
201    /// attribute's declaring class, so they stay valid); otherwise it is kept
202    /// beside it. Returns whether the class changed.
203    pub(crate) fn add_type(&mut self, other: &'static ClassDef, reg: &TypeRegistry) -> bool {
204        if self.types().any(|c| c.qualified == other.qualified) {
205            return false;
206        }
207        let promote = reg.chain(other).iter().any(|c| c.qualified == self.class.qualified);
208        let kept = if promote { std::mem::replace(&mut self.class, other) } else { other };
209        self.also.get_or_insert_with(Default::default).push(kept);
210        promote
211    }
212
213    /// Whether some file defined the object (`rdf:ID`) rather than only
214    /// describing one defined elsewhere (`rdf:about`).
215    pub fn defined(&self) -> bool {
216        !self.described
217    }
218
219    /// Record that the file only described the object (`rdf:about`).
220    pub fn mark_described(&mut self) {
221        self.described = true;
222    }
223
224    pub fn mrid(&self) -> &str {
225        &self.mrid
226    }
227
228    /// Family-qualified class name, e.g. `"Terminal"` (CGMES) or
229    /// `"nc:Contingency"`. This is the `CimDataset::by_type` key.
230    pub fn type_name(&self) -> &'static str {
231        self.class.qualified
232    }
233
234    /// Bare CIM class name, without the family qualifier.
235    pub fn local_name(&self) -> &'static str {
236        self.class.local
237    }
238
239    /// RDF namespace IRI of the declaring class, trailing delimiter included.
240    pub fn type_ns(&self) -> &'static str {
241        self.class.ns
242    }
243
244    pub fn fields(&self) -> &FieldMap {
245        &self.fields
246    }
247
248    pub fn duplicate_fields(&self) -> &FastSet<&'static str> {
249        &self.duplicate_fields
250    }
251
252    /// The `xml:lang` tags of `key`'s text values, one per value that had
253    /// one. A value written without a tag has no entry, so a field whose
254    /// values outnumber its tags has an untagged value.
255    pub fn langs(&self, key: &str) -> impl Iterator<Item = &str> {
256        self.langs.iter().flat_map(|l| l.iter()).filter(move |(k, _)| *k == key).map(|(_, l)| &**l)
257    }
258
259    /// Record the `xml:lang` of a text value just added to `key`.
260    pub fn add_lang(&mut self, key: &'static str, lang: &str) {
261        self.langs.get_or_insert_with(Default::default).push((key, lang.into()));
262    }
263
264    /// Hold `key`'s values as references, or as text: what a caller knows
265    /// that the class table does not ([`crate::CimDataset::element_from_json`]).
266    pub fn retype(&mut self, key: &str, reference: bool) {
267        if let Some(value) = self.fields.get_mut(key) {
268            *value = match (std::mem::replace(value, FieldValue::Text(String::new())), reference) {
269                (FieldValue::Text(s), true) => FieldValue::Resource(s),
270                (FieldValue::TextList(v), true) => FieldValue::ResourceList(v),
271                (FieldValue::Resource(s), false) => FieldValue::Text(s),
272                (FieldValue::ResourceList(v), false) => FieldValue::TextList(v),
273                (other, _) => other,
274            };
275        }
276    }
277
278    pub fn get(&self, attr: &str) -> Option<&FieldValue> {
279        self.fields.get(attr)
280    }
281
282    pub fn get_str(&self, attr: &str) -> Option<&str> {
283        match self.fields.get(attr) {
284            Some(FieldValue::Text(s)) => Some(s.as_str()),
285            Some(FieldValue::TextList(v)) => v.first().map(String::as_str),
286            _ => None,
287        }
288    }
289
290    pub fn get_f64(&self, attr: &str) -> Option<f64> {
291        self.get_str(attr)?.parse().ok()
292    }
293
294    pub fn get_bool(&self, attr: &str) -> Option<bool> {
295        match self.get_str(attr)? {
296            "true" | "1" => Some(true),
297            "false" | "0" => Some(false),
298            _ => None,
299        }
300    }
301
302    /// Referenced MRIDs.
303    ///
304    /// Always a slice, even where the schema says the association is 1:1 — CGMES
305    /// has widened 1:1 to 1:many between versions, and a caller written against
306    /// a slice does not have to change when that happens.
307    pub fn get_refs(&self, attr: &str) -> &[String] {
308        match self.fields.get(attr) {
309            Some(FieldValue::Resource(s)) => std::slice::from_ref(s),
310            Some(FieldValue::ResourceList(v)) => v.as_slice(),
311            _ => &[],
312        }
313    }
314
315    /// First referenced MRID, for associations known to be single-valued.
316    pub fn get_ref(&self, attr: &str) -> Option<&str> {
317        self.get_refs(attr).first().map(String::as_str)
318    }
319
320    /// Add one value as the decoder reads it: a repeated key becomes a list
321    /// and is recorded in [`Element::duplicate_fields`].
322    pub fn add_field(&mut self, key: &'static str, val: FieldValue) {
323        match &val {
324            FieldValue::Resource(new_ref) => match self.fields.get_mut(key) {
325                Some(FieldValue::ResourceList(list)) => {
326                    list.push(new_ref.clone());
327                    self.duplicate_fields.insert(key);
328                    return;
329                }
330                Some(existing @ FieldValue::Resource(_)) => {
331                    let FieldValue::Resource(old) = std::mem::replace(existing, FieldValue::ResourceList(Vec::new())) else {
332                        unreachable!()
333                    };
334                    *existing = FieldValue::ResourceList(vec![old, new_ref.clone()]);
335                    self.duplicate_fields.insert(key);
336                    return;
337                }
338                _ => {}
339            },
340            FieldValue::Text(new_text) => match self.fields.get_mut(key) {
341                Some(FieldValue::TextList(list)) => {
342                    list.push(new_text.clone());
343                    self.duplicate_fields.insert(key);
344                    return;
345                }
346                Some(existing @ FieldValue::Text(_)) => {
347                    let FieldValue::Text(old) = std::mem::replace(existing, FieldValue::TextList(Vec::new())) else {
348                        unreachable!()
349                    };
350                    *existing = FieldValue::TextList(vec![old, new_text.clone()]);
351                    self.duplicate_fields.insert(key);
352                    return;
353                }
354                _ => {}
355            },
356            _ => {
357                if self.fields.contains_key(key) {
358                    self.duplicate_fields.insert(key);
359                }
360            }
361        }
362        self.fields.insert(key, val);
363    }
364
365    /// Combine another file's view of the same object: a later scalar wins,
366    /// reference lists are joined. Duplicate tracking stays per file.
367    pub fn merge_from(&mut self, other: &Element) {
368        self.described &= other.described;
369        for (k, v) in &other.fields {
370            match v {
371                FieldValue::ResourceList(new_list) => match self.fields.get_mut(k) {
372                    Some(FieldValue::ResourceList(existing)) => existing.extend(new_list.iter().cloned()),
373                    _ => {
374                        self.fields.insert(*k, v.clone());
375                    }
376                },
377                _ => {
378                    self.fields.insert(*k, v.clone());
379                    // The later file's text replaced ours, tags included.
380                    if let Some(l) = self.langs.as_mut() {
381                        l.retain(|(key, _)| key != k);
382                    }
383                    for lang in other.langs(k) {
384                        self.add_lang(k, lang);
385                    }
386                }
387            }
388        }
389    }
390
391    /// `{"id": mRID, "Class.attr": value, ...}`: a value is a string, a
392    /// repeated one an array of strings; when any text value had an
393    /// `xml:lang`, `"_lang"`: key → its tags in value order.
394    pub fn to_json_value(&self) -> serde_json::Value {
395        let mut map = serde_json::Map::with_capacity(self.fields.len() + 2);
396        map.insert("id".to_string(), serde_json::Value::String(self.mrid.clone()));
397        for (k, v) in &self.fields {
398            map.insert(k.to_string(), field_to_json(v));
399        }
400        if let Some(langs) = &self.langs {
401            let mut tags = serde_json::Map::new();
402            for (key, lang) in langs.iter() {
403                if let serde_json::Value::Array(list) =
404                    tags.entry(key.to_string()).or_insert_with(|| serde_json::Value::Array(Vec::new()))
405                {
406                    list.push(serde_json::Value::String(lang.to_string()));
407                }
408            }
409            map.insert(LANG_KEY.to_string(), serde_json::Value::Object(tags));
410        }
411        serde_json::Value::Object(map)
412    }
413
414    /// The inverse of [`Element::to_json_value`]. Whether a string is a
415    /// reference or text comes from the attribute's declaration; an attribute
416    /// the class does not declare is read as text.
417    pub fn from_json(
418        class: &'static ClassDef,
419        reg: &TypeRegistry,
420        value: &serde_json::Value,
421    ) -> Result<Self, String> {
422        let obj = value.as_object().ok_or("element is not a JSON object")?;
423        let mrid = obj.get("id").and_then(|v| v.as_str()).ok_or("element has no \"id\"")?.to_string();
424        let mut fields = FieldMap::default();
425        for (k, v) in obj {
426            if k == "id" || k == "_type" || k == LANG_KEY {
427                continue;
428            }
429            let is_ref = reg.attr(class, k).is_some_and(|a| a.kind != AttrKind::Literal);
430            let value = match v {
431                serde_json::Value::String(s) if is_ref => FieldValue::Resource(s.clone()),
432                serde_json::Value::String(s) => FieldValue::Text(s.clone()),
433                serde_json::Value::Array(items) => {
434                    let items: Vec<String> = items
435                        .iter()
436                        .map(|i| i.as_str().map(str::to_string).ok_or(format!("{k}: list item is not a string")))
437                        .collect::<Result<_, _>>()?;
438                    if is_ref { FieldValue::ResourceList(items) } else { FieldValue::TextList(items) }
439                }
440                other => return Err(format!("{k}: expected a string or an array, got {other}")),
441            };
442            fields.insert(reg.field_key(class, k), value);
443        }
444        let mut element = Self::with_fields(class, mrid, fields);
445        if let Some(tags) = obj.get(LANG_KEY).and_then(|t| t.as_object()) {
446            for (k, list) in tags {
447                let key = reg.field_key(class, k);
448                for lang in list.as_array().into_iter().flatten().filter_map(|l| l.as_str()) {
449                    element.add_lang(key, lang);
450                }
451            }
452        }
453        Ok(element)
454    }
455}
456
457/// The JSON key of an element's language tags ([`Element::to_json_value`]).
458pub const LANG_KEY: &str = "_lang";
459
460fn field_to_json(v: &FieldValue) -> serde_json::Value {
461    match v {
462        FieldValue::Text(s) | FieldValue::Resource(s) => serde_json::Value::String(s.clone()),
463        FieldValue::TextList(v) | FieldValue::ResourceList(v) => {
464            serde_json::Value::Array(v.iter().cloned().map(serde_json::Value::String).collect())
465        }
466    }
467}
468
469// --- Classes -----------------------------------------------------------------
470
471/// How an attribute's value is carried in RDF/XML.
472#[derive(Debug, Clone, Copy, PartialEq, Eq)]
473pub enum AttrKind {
474    /// A literal, written as element text.
475    Literal,
476    /// A reference to another object, written as `rdf:resource`.
477    Association,
478    /// An enumeration value, written as `rdf:resource` to a vocabulary IRI.
479    Enum,
480}
481
482/// One attribute of a class, as the schema declares it.
483#[derive(Debug)]
484pub struct AttrDef {
485    /// `Class.attr`, matching the key in [`Element::fields`].
486    pub id: &'static str,
487    /// The attribute's RDF namespace: its predicate IRI is `ns` + `id`.
488    pub ns: &'static str,
489    pub kind: AttrKind,
490    /// The CIM datatype of a literal; the class an association or enumeration
491    /// points to.
492    pub range: &'static str,
493    pub is_list: bool,
494    /// Whether a profile exchanges this end of the association — `false` for the
495    /// inverse ends the vocabularies declare but no dataset writes. Literals and
496    /// enumerations are always used.
497    pub used: bool,
498    /// The XSD datatype of a literal, as the local name after
499    /// `http://www.w3.org/2001/XMLSchema#` (`"double"`); `""` otherwise.
500    pub xsd: &'static str,
501    /// The namespace of an enumeration's values (the decoder keeps a value after
502    /// its `#`); `""` otherwise.
503    pub value_ns: &'static str,
504    /// Profile codes that carry this attribute.
505    pub origins: &'static [&'static str],
506    /// The vocabulary's `rdfs:comment`, HTML removed and on one line; `""`
507    /// if it has none.
508    pub comment: &'static str,
509}
510
511/// What one family's schema says, as a table: its classes, the profiles it
512/// defines and the namespaces its vocabularies bind. Generated by `cimgen`, or
513/// read from RDFS at runtime (`schema_source`).
514#[derive(Debug)]
515pub struct Schema {
516    pub classes: &'static [ClassDef],
517    /// Profile code → profile URI, the value a dataset's header declares; sorted
518    /// by code.
519    pub profiles: &'static [(&'static str, &'static str)],
520    /// XML prefix → namespace IRI; sorted by prefix.
521    pub namespaces: &'static [(&'static str, &'static str)],
522    /// Enumerations, sorted by name.
523    pub enums: &'static [EnumDef],
524}
525
526impl Schema {
527    /// The enumeration called `name` (`WindingConnection`).
528    pub fn enumeration(&self, name: &str) -> Option<&'static EnumDef> {
529        let enums: &'static [EnumDef] = self.enums;
530        enums.binary_search_by_key(&name, |e| e.local).ok().map(|i| &enums[i])
531    }
532
533    /// The enumeration value `id` names, as the decoder keeps it
534    /// (`WindingConnection.D`), with its enumeration.
535    pub fn enum_value(&self, id: &str) -> Option<(&'static EnumDef, &'static EnumValueDef)> {
536        let e = self.enumeration(id.split_once('.')?.0)?;
537        Some((e, e.values.iter().find(|v| v.id == id)?))
538    }
539}
540
541/// An enumeration of the schema and the values it allows.
542#[derive(Debug)]
543pub struct EnumDef {
544    pub ns: &'static str,
545    pub local: &'static str,
546    /// The vocabulary's `rdfs:comment`, as for [`ClassDef::comment`].
547    pub comment: &'static str,
548    /// In vocabulary order.
549    pub values: &'static [EnumValueDef],
550}
551
552/// One value of an enumeration.
553#[derive(Debug)]
554pub struct EnumValueDef {
555    /// `Enum.value`, as the decoder keeps a value after its `#`.
556    pub id: &'static str,
557    /// The vocabulary's `rdfs:comment`, as for [`ClassDef::comment`].
558    pub comment: &'static str,
559}
560
561/// A class of the schema: what it is called and what it declares.
562#[derive(Debug)]
563pub struct ClassDef {
564    pub ns: &'static str,
565    pub local: &'static str,
566    /// Family-qualified name; the `CimDataset::by_type` key.
567    pub qualified: &'static str,
568    /// Index of the super class within the same table.
569    pub super_class: Option<usize>,
570    pub concrete: bool,
571    /// The attributes this class declares itself, unused association ends
572    /// included ([`AttrDef::used`]); see [`TypeRegistry::attr`] for inherited
573    /// ones.
574    pub attrs: &'static [AttrDef],
575    pub origins: &'static [&'static str],
576    /// The vocabulary's `rdfs:comment`, HTML removed and on one line; `""`
577    /// if it has none.
578    pub comment: &'static str,
579}
580
581impl ClassDef {
582    /// An attribute this class declares itself (not an inherited one).
583    pub fn attr(&self, id: &str) -> Option<&'static AttrDef> {
584        self.attrs.iter().find(|a| a.id == id)
585    }
586}
587
588/// Namespace-aware class lookup, and every attribute each class declares.
589///
590/// RDF/XML identifies a class by `(namespace, local name)`, and the two profile
591/// families overlap on 164 local names — both even spell the prefix `cim`, so
592/// only the xmlns binding distinguishes them. Lookup is therefore keyed on the
593/// resolved namespace, with a bare-name fallback (CGMES only) that preserves
594/// the historical behaviour for files whose prefixes are unbound or unexpected.
595#[derive(Default)]
596pub struct TypeRegistry {
597    by_ns: FastMap<&'static str, FastMap<&'static str, &'static ClassDef>>,
598    bare: FastMap<&'static str, &'static ClassDef>,
599    by_type_name: FastMap<&'static str, &'static ClassDef>,
600    /// Qualified class name → every attribute it declares, inherited ones
601    /// included. Built once from the class tables.
602    declared: FastMap<&'static str, FastMap<&'static str, &'static AttrDef>>,
603    /// Qualified class name → the class and its ancestors, root first.
604    chain: FastMap<&'static str, Vec<&'static ClassDef>>,
605    /// Each registered family, in registration order.
606    families: Vec<FamilyTable>,
607}
608
609/// One family as registered: its schema, the `by_type` prefix its classes
610/// carry (`""` for CGMES, `"nc:"`), and its attributes by id.
611struct FamilyTable {
612    id: &'static str,
613    prefix: &'static str,
614    schema: &'static Schema,
615    /// Attribute id → its first declaration in table order. An id names one
616    /// predicate, whichever class declares it.
617    attrs: FastMap<&'static str, &'static AttrDef>,
618}
619
620impl TypeRegistry {
621    pub fn new() -> Self {
622        Self::default()
623    }
624
625    /// Register a family's schema under `id`. `bare_fallback` adds its classes
626    /// to the bare-name fallback: an unbound prefix means the families cannot
627    /// be told apart, so only the default family (CGMES) takes it.
628    pub fn add_family(&mut self, id: &'static str, schema: &'static Schema, bare_fallback: bool) {
629        let classes = schema.classes;
630        let prefix = classes
631            .first()
632            .and_then(|c| c.qualified.strip_suffix(c.local))
633            .unwrap_or("");
634        let mut by_id = FastMap::default();
635        for class in classes {
636            for a in class.attrs {
637                by_id.entry(a.id).or_insert(a);
638            }
639        }
640        self.families.push(FamilyTable { id, prefix, schema, attrs: by_id });
641        for class in classes {
642            self.by_ns.entry(class.ns).or_default().insert(class.local, class);
643            self.by_type_name.insert(class.qualified, class);
644            if bare_fallback {
645                self.bare.insert(class.local, class);
646            }
647            let mut attrs = FastMap::default();
648            let mut chain = Vec::new();
649            let mut next = Some(class);
650            while let Some(c) = next {
651                for a in c.attrs {
652                    attrs.entry(a.id).or_insert(a);
653                }
654                chain.push(c);
655                next = c.super_class.map(|i| &classes[i]);
656            }
657            chain.reverse();
658            self.declared.insert(class.qualified, attrs);
659            self.chain.insert(class.qualified, chain);
660        }
661    }
662
663    /// Lookup table for one namespace, resolved once per XML prefix per file.
664    pub fn ns_table(&self, ns: &str) -> Option<&FastMap<&'static str, &'static ClassDef>> {
665        self.by_ns.get(ns)
666    }
667
668    pub fn bare(&self) -> &FastMap<&'static str, &'static ClassDef> {
669        &self.bare
670    }
671
672    pub fn by_type_name(&self, name: &str) -> Option<&'static ClassDef> {
673        self.by_type_name.get(name).copied()
674    }
675
676    /// The attribute `id` if `class` declares it, itself or by inheritance.
677    pub fn attr(&self, class: &ClassDef, id: &str) -> Option<&'static AttrDef> {
678        self.declared.get(class.qualified)?.get(id).copied()
679    }
680
681    /// The attributes `class` declares, inherited ones included, by id — for a
682    /// caller that resolves many keys of one element.
683    pub fn declared(&self, class: &ClassDef) -> Option<&FastMap<&'static str, &'static AttrDef>> {
684        self.declared.get(class.qualified)
685    }
686
687    /// The `&'static` key a field `id` of `class` is stored under.
688    pub fn field_key(&self, class: &ClassDef, id: &str) -> &'static str {
689        self.attr(class, id).map_or_else(|| intern(id), |a| a.id)
690    }
691
692    /// `class` and its ancestors, root first.
693    pub fn chain(&self, class: &ClassDef) -> &[&'static ClassDef] {
694        self.chain.get(class.qualified).map_or(&[], Vec::as_slice)
695    }
696
697    /// The `by_type` keys of the concrete classes at or below `qualified`,
698    /// sorted: what instances of a class can be typed as.
699    pub fn concrete_descendants(&self, qualified: &str) -> Vec<&'static str> {
700        let mut out: Vec<&'static str> = self
701            .chain
702            .iter()
703            .filter(|(_, chain)| chain.iter().any(|c| c.qualified == qualified))
704            .filter_map(|(q, _)| self.by_type_name.get(q).filter(|c| c.concrete).map(|c| c.qualified))
705            .collect();
706        out.sort_unstable();
707        out
708    }
709
710    /// Every attribute `class` declares, inherited ones included.
711    pub fn attrs(&self, class: &ClassDef) -> impl Iterator<Item = &'static AttrDef> + '_ {
712        self.declared.get(class.qualified).into_iter().flat_map(|m| m.values().copied())
713    }
714
715    /// The schema registered as `family` (`"cgmes"`, `"nc"`).
716    pub fn schema(&self, family: &str) -> Option<&'static Schema> {
717        self.family(family).map(|f| f.schema)
718    }
719
720    fn family(&self, id: &str) -> Option<&FamilyTable> {
721        self.families.iter().find(|f| f.id == id)
722    }
723
724    /// The enumeration value `id` (`WindingConnection.D`) in the family of
725    /// `class`, which an element of `class` uses it on.
726    pub fn enum_value(&self, class: &ClassDef, id: &str) -> Option<(&'static EnumDef, &'static EnumValueDef)> {
727        self.schema(self.family_of(class.qualified)?)?.enum_value(id)
728    }
729
730    /// The family a `by_type` key belongs to, by its prefix.
731    pub fn family_of(&self, type_name: &str) -> Option<&'static str> {
732        // The longest matching prefix, so `nc:X` is not taken for CGMES's "".
733        self.families
734            .iter()
735            .filter(|f| type_name.starts_with(f.prefix))
736            .max_by_key(|f| f.prefix.len())
737            .map(|f| f.id)
738    }
739
740    /// The attribute `id` as `family` declares it on any class — what a field
741    /// key names when the element's own class does not declare it.
742    pub fn family_attr(&self, family: &str, id: &str) -> Option<&'static AttrDef> {
743        self.family(family)?.attrs.get(id).copied()
744    }
745
746    /// The attribute a field `key` of an element of `class` stands for: the
747    /// class's own declaration, else the family's.
748    pub fn attr_of(&self, class: &ClassDef, key: &str) -> Option<&'static AttrDef> {
749        self.attr(class, key)
750            .or_else(|| self.family_attr(self.family_of(class.qualified)?, key))
751    }
752}