Skip to main content

cimmodel/
decode.rs

1use std::path::Path;
2
3use quick_xml::events::{BytesStart, Event};
4use quick_xml::Reader;
5
6use crate::base::{intern, AttrDef, ClassDef, Element, FastMap, FastSet, FieldValue, TypeRegistry};
7use crate::registry;
8
9pub struct CimDataset {
10    pub entries: FastMap<String, Element>,
11    /// Maps `type_name()` → list of MRIDs of that type. Populated on insert, maintained on merge.
12    pub by_type: FastMap<String, Vec<String>>,
13}
14
15impl Default for CimDataset {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl CimDataset {
22    pub fn new() -> Self {
23        Self {
24            entries: FastMap::default(),
25            by_type: FastMap::default(),
26        }
27    }
28
29    /// Decode an RDF/XML string into a CimDataset, using the provided type registry.
30    pub fn decode_str(content: &str) -> Result<Self, Box<dyn std::error::Error>> {
31        let mut ds = Self::new();
32        parse_rdf(content, registry::type_registry(), &mut ds)?;
33        Ok(ds)
34    }
35
36    /// Decode an RDF/XML file into a CimDataset, using the provided type registry.
37    pub fn decode_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
38        Self::decode_str(&std::fs::read_to_string(path)?)
39    }
40
41    /// Decode multiple RDF/XML files into a single CimDataset, merging entries with the same MRID.
42    pub fn decode_files(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
43        let mut combined = Self::new();
44        for path in paths {
45            let file_ds = Self::decode_file(path)?;
46            combined.merge(file_ds);
47        }
48        Ok(combined)
49    }
50
51    /// Decode files in parallel using one thread per file, without merging.
52    /// Preserves input order. Falls back to sequential decoding for 0–1 paths
53    /// to avoid thread-spawn overhead.
54    pub fn decode_files_parallel_separate(paths: &[&Path]) -> Result<Vec<Self>, Box<dyn std::error::Error>> {
55        if paths.len() <= 1 {
56            return paths.iter().map(|p| Self::decode_file(p)).collect();
57        }
58        let results: Vec<Result<Self, String>> = std::thread::scope(|s| {
59            paths
60                .iter()
61                .map(|p| s.spawn(|| Self::decode_file(p).map_err(|e| e.to_string())))
62                .collect::<Vec<_>>()
63                .into_iter()
64                .map(|h| h.join().expect("decode thread panicked"))
65                .collect()
66        });
67        results
68            .into_iter()
69            .collect::<Result<_, String>>()
70            .map_err(|e| e.into())
71    }
72
73    /// Decode files in parallel using one thread per file, then merge sequentially.
74    pub fn decode_files_parallel(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
75        let datasets = Self::decode_files_parallel_separate(paths)?;
76        Ok(datasets
77            .into_iter()
78            .reduce(|mut a, b| {
79                a.merge(b);
80                a
81            })
82            .unwrap_or_default())
83    }
84
85    /// Merge another dataset into self, combining objects with the same MRID:
86    /// a later scalar wins, reference lists are joined. An object keeps the
87    /// class it was first decoded as.
88    ///
89    /// Where the files type an object differently — an SSH file writing
90    /// `cim:Equipment` for what the EQ file wrote as `cim:ACLineSegment` — the
91    /// object keeps every type, as the RDF union does ([`Element::types`]),
92    /// and its class (and `by_type` bucket) is the most specific, whichever
93    /// file came first. Of unrelated classes the first stays the class.
94    pub fn merge(&mut self, other: CimDataset) {
95        let reg = registry::type_registry();
96        let mut moved: FastMap<&'static str, FastSet<String>> = FastMap::default();
97        for (mrid, incoming) in other.entries {
98            if let Some(existing) = self.entries.get_mut(&mrid) {
99                existing.merge_from(&incoming);
100                let old = existing.class();
101                let mut changed = false;
102                for t in incoming.types() {
103                    changed |= existing.add_type(t, reg);
104                }
105                if changed {
106                    moved.entry(old.qualified).or_default().insert(mrid.clone());
107                    self.by_type.entry(existing.type_name().to_string()).or_default().push(mrid);
108                }
109            } else {
110                let type_name = incoming.type_name().to_string();
111                self.by_type.entry(type_name).or_default().push(mrid.clone());
112                self.entries.insert(mrid, incoming);
113            }
114        }
115        // One pass per bucket left, however many of its objects moved.
116        for (old, mrids) in moved {
117            if let Some(bucket) = self.by_type.get_mut(old) {
118                bucket.retain(|m| !mrids.contains(m));
119            }
120        }
121    }
122
123    /// Insert or replace the element at `mrid`, keeping `by_type` consistent
124    /// (including the case where `mrid` already exists under a different type).
125    pub fn set(&mut self, mrid: String, element: Element) {
126        let new_type = element.type_name().to_string();
127        if let Some(old) = self.entries.get(&mrid) {
128            let old_type = old.type_name();
129            if old_type != new_type
130                && let Some(v) = self.by_type.get_mut(old_type) {
131                    v.retain(|m| m != &mrid);
132                }
133        }
134        let already_indexed = self
135            .by_type
136            .get(&new_type)
137            .is_some_and(|v| v.contains(&mrid));
138        if !already_indexed {
139            self.by_type.entry(new_type).or_default().push(mrid.clone());
140        }
141        self.entries.insert(mrid, element);
142    }
143
144    /// Remove the element at `mrid`, pruning it from `by_type`. Returns the
145    /// removed element, or `None` if `mrid` was not present.
146    pub fn remove(&mut self, mrid: &str) -> Option<Element> {
147        let removed = self.entries.remove(mrid)?;
148        if let Some(v) = self.by_type.get_mut(removed.type_name()) {
149            v.retain(|m| m != mrid);
150        }
151        Some(removed)
152    }
153}
154
155// --- XML streaming parser ---------------------------------------------------
156
157/// Push an MRID onto its type bucket, allocating the key only when the bucket
158/// is new rather than once per element.
159fn index(by_type: &mut FastMap<String, Vec<String>>, type_name: &'static str, mrid: String) {
160    if let Some(bucket) = by_type.get_mut(type_name) {
161        bucket.push(mrid);
162    } else {
163        by_type.insert(type_name.to_string(), vec![mrid]);
164    }
165}
166
167type Table = FastMap<&'static str, &'static ClassDef>;
168
169/// The document's `xmlns` bindings, resolved once to dispatch tables.
170///
171/// Resolving a prefix per element rather than a namespace IRI is what keeps
172/// this cheap: prefixes are two to four bytes, IRIs are thirty-odd, and a
173/// document binds a handful of them and then repeats them on every element.
174#[derive(Default)]
175struct Scope {
176    prefixes: Vec<(Vec<u8>, Option<&'static Table>)>,
177    default: Option<&'static Table>,
178}
179
180impl Scope {
181    /// Collect `xmlns:*` and `xmlns` declarations from an element.
182    fn absorb(&mut self, e: &BytesStart, reg: &'static TypeRegistry) {
183        for attr in e.attributes().flatten() {
184            let key = attr.key.as_ref();
185            let Some(rest) = key.strip_prefix(b"xmlns".as_slice()) else {
186                continue;
187            };
188            let Ok(ns) = std::str::from_utf8(&attr.value) else {
189                continue;
190            };
191            // A namespace written without its delimiter still names the same vocabulary.
192            let table = reg.ns_table(ns).or_else(|| {
193                if ns.ends_with('#') || ns.ends_with('/') {
194                    None
195                } else {
196                    reg.ns_table(&format!("{ns}#"))
197                }
198            });
199            match rest.strip_prefix(b":".as_slice()) {
200                Some(prefix) => {
201                    let prefix = prefix.to_vec();
202                    match self.prefixes.iter_mut().find(|(k, _)| *k == prefix) {
203                        Some(slot) => slot.1 = table,
204                        None => self.prefixes.push((prefix, table)),
205                    }
206                }
207                None if rest.is_empty() => self.default = table,
208                None => {}
209            }
210        }
211    }
212
213    fn table(&self, prefix: Option<&[u8]>) -> Option<&'static Table> {
214        match prefix {
215            None => self.default,
216            Some(p) => self
217                .prefixes
218                .iter()
219                .find(|(k, _)| k.as_slice() == p)
220                .and_then(|(_, t)| *t),
221        }
222    }
223
224}
225
226/// One pass over an element's attributes, yielding both things the parser needs:
227/// the MRID and whether the element redeclares any namespace. Scanning twice
228/// costs more than the nested-`xmlns` case it would serve, because every element
229/// pays for it and almost none declare one.
230fn scan_attrs(e: &BytesStart) -> Result<(String, bool), Box<dyn std::error::Error>> {
231    let mut mrid = String::new();
232    let mut has_xmlns = false;
233    for attr in e.attributes().flatten() {
234        let key = attr.key.as_ref();
235        if key == b"rdf:about" || key == b"rdf:ID" {
236            mrid = strip_fragment(std::str::from_utf8(&attr.value)?);
237        } else if key.starts_with(b"xmlns") {
238            has_xmlns = true;
239        }
240    }
241    Ok((mrid, has_xmlns))
242}
243
244/// Split a qualified name into its prefix and local part.
245fn split_qname(raw: &[u8]) -> (Option<&[u8]>, &[u8]) {
246    match raw.iter().position(|b| *b == b':') {
247        Some(i) => (Some(&raw[..i]), &raw[i + 1..]),
248        None => (None, raw),
249    }
250}
251
252/// Resolve an element name to its class.
253///
254/// The namespace wins when the document binds one we know. Otherwise we fall
255/// back to the bare local name, which is what every pre-namespace release did:
256/// real files bind prefixes we do not expect (one CGMES test file binds the
257/// ModelDescription IRI to `mdc`), and write elements under a namespace their
258/// class was not declared in (`<cim:BoundaryPoint>` where `BoundaryPoint` is
259/// declared in `CIM100-European#`). The fallback is therefore per element, not
260/// per document.
261fn resolve(
262    scope: &Scope,
263    reg: &'static TypeRegistry,
264    raw: &[u8],
265) -> Result<Option<&'static ClassDef>, Box<dyn std::error::Error>> {
266    let (prefix, local_bytes) = split_qname(raw);
267    let local = std::str::from_utf8(local_bytes)?;
268    if let Some(class) = scope.table(prefix).and_then(|t| t.get(local)) {
269        return Ok(Some(*class));
270    }
271    Ok(reg.bare().get(local).copied())
272}
273
274/// Parse an RDF/XML string into a CimDataset, using the provided type registry.
275fn parse_rdf(
276    content: &str,
277    reg: &'static TypeRegistry,
278    ds: &mut CimDataset,
279) -> Result<(), Box<dyn std::error::Error>> {
280    let mut reader = Reader::from_str(content);
281    let mut buf = Vec::new();
282
283    let mut depth: u32 = 0;
284    let mut scope = Scope::default();
285    // Outer scope parked while a nested `xmlns` override is in effect.
286    let mut nested: Option<Scope> = None;
287    // Resolved at the element's start tag, so an unregistered element never
288    // accumulates fields.
289    let mut current: Option<Element> = None;
290    // The attributes `current`'s class declares, which supply most field keys.
291    let mut declared: Option<&'static FastMap<&'static str, &'static AttrDef>> = None;
292    let mut pending_key: Option<&'static str> = None;
293    // The `xml:lang` of the field element whose text comes next.
294    let mut pending_lang: Option<String> = None;
295    // A field key as stored: the class table's id when the class declares it,
296    // else the interned name.
297    let key_of = |declared: Option<&'static FastMap<&'static str, &'static AttrDef>>, local: &str| {
298        declared.and_then(|d| d.get(local)).map_or_else(|| intern(local), |a| a.id)
299    };
300
301    loop {
302        match reader.read_event_into(&mut buf) {
303            Ok(Event::Start(ref e)) => {
304                depth += 1;
305
306                match depth {
307                    1 => scope.absorb(e, reg),
308                    2 => {
309                        // Every CGMES and NC file declares its namespaces on the
310                        // root, but a nested redeclaration is legal RDF/XML, so
311                        // honour one when it appears. The End arm restores the
312                        // outer scope.
313                        let (mrid, has_xmlns) = scan_attrs(e)?;
314                        if has_xmlns {
315                            let mut inner = Scope {
316                                prefixes: scope.prefixes.clone(),
317                                default: scope.default,
318                            };
319                            inner.absorb(e, reg);
320                            nested = Some(std::mem::replace(&mut scope, inner));
321                        }
322
323                        current = resolve(&scope, reg, e.name().as_ref())?.map(|class| Element::new(class, mrid));
324                        declared = current.as_ref().and_then(|c| reg.declared(c.class()));
325                    }
326                    3 => {
327                        if let Some(element) = current.as_mut() {
328                            let name = e.name();
329                            let (_, local_bytes) = split_qname(name.as_ref());
330                            let local = key_of(declared, std::str::from_utf8(local_bytes)?);
331                            let (res, lang) = field_attrs(e.attributes())?;
332                            if let Some(res) = res {
333                                element.add_field(local, FieldValue::Resource(res));
334                            } else {
335                                pending_key = Some(local);
336                                pending_lang = lang;
337                            }
338                        }
339                    }
340                    _ => {}
341                }
342            }
343
344            Ok(Event::Empty(ref e)) => {
345                match depth {
346                    1 => {
347                        // Top-level self-closing element: <cim:Foo rdf:ID="x" />
348                        // It can carry its own xmlns; with no children there is
349                        // nothing to restore afterwards, so resolve against a
350                        // throwaway scope.
351                        let (mrid, has_xmlns) = scan_attrs(e)?;
352                        let local_scope = if has_xmlns {
353                            let mut inner = Scope {
354                                prefixes: scope.prefixes.clone(),
355                                default: scope.default,
356                            };
357                            inner.absorb(e, reg);
358                            Some(inner)
359                        } else {
360                            None
361                        };
362                        let active = local_scope.as_ref().unwrap_or(&scope);
363                        if !mrid.is_empty()
364                            && let Some(class) = resolve(active, reg, e.name().as_ref())? {
365                                index(&mut ds.by_type, class.qualified, mrid.clone());
366                                ds.entries.insert(mrid.clone(), Element::new(class, mrid));
367                            }
368                    }
369                    2 => {
370                        // Self-closing field element within the current type block.
371                        if let Some(element) = current.as_mut()
372                            && let Some(res) = find_resource(e.attributes())? {
373                                let name = e.name();
374                                let (_, local_bytes) = split_qname(name.as_ref());
375                                let local = key_of(declared, std::str::from_utf8(local_bytes)?);
376                                element.add_field(local, FieldValue::Resource(res));
377                            }
378                    }
379                    _ => {}
380                }
381            }
382
383            Ok(Event::Text(ref e)) => {
384                if depth == 3
385                    && let (Some(element), Some(key)) = (current.as_mut(), pending_key.take()) {
386                        let text = e.unescape()?.trim().to_string();
387                        if !text.is_empty() {
388                            element.add_field(key, FieldValue::Text(text));
389                            if let Some(lang) = pending_lang.take() {
390                                element.add_lang(key, &lang);
391                            }
392                        }
393                    }
394            }
395
396            Ok(Event::End(_)) => {
397                if depth == 2 {
398                    pending_key = None;
399                    pending_lang = None;
400                    if let Some(element) = current.take()
401                        && !element.mrid().is_empty() {
402                            index(&mut ds.by_type, element.type_name(), element.mrid().to_string());
403                            ds.entries.insert(element.mrid().to_string(), element);
404                        }
405                    if let Some(outer) = nested.take() {
406                        scope = outer;
407                    }
408                }
409                depth = depth.saturating_sub(1);
410            }
411
412            Ok(Event::Eof) => break,
413            Err(e) => return Err(Box::new(e)),
414            _ => {}
415        }
416        buf.clear();
417    }
418
419    Ok(())
420}
421
422// --- helpers ----------------------------------------------------------------
423
424
425/// Strip the fragment from a URI, e.g. "http://example.com#foo" → "foo".
426fn strip_fragment(s: &str) -> String {
427    if let Some(i) = s.rfind('#') {
428        s[i + 1..].to_string()
429    } else {
430        s.trim_start_matches('#').to_string()
431    }
432}
433
434
435/// A field element's `rdf:resource` (as [`find_resource`] gives it) and
436/// `xml:lang`, in one pass over its attributes.
437fn field_attrs(
438    attrs: quick_xml::events::attributes::Attributes<'_>,
439) -> Result<(Option<String>, Option<String>), Box<dyn std::error::Error>> {
440    let (mut res, mut lang) = (None, None);
441    for attr in attrs.flatten() {
442        match attr.key.as_ref() {
443            b"rdf:resource" => res = Some(strip_fragment(std::str::from_utf8(&attr.value)?)),
444            b"xml:lang" => lang = Some(std::str::from_utf8(&attr.value)?.to_string()),
445            _ => {}
446        }
447    }
448    Ok((res, lang))
449}
450
451/// Find the value of the "rdf:resource" attribute from an element.
452fn find_resource(
453    attrs: quick_xml::events::attributes::Attributes<'_>,
454) -> Result<Option<String>, Box<dyn std::error::Error>> {
455    for attr in attrs.flatten() {
456        let key = std::str::from_utf8(attr.key.as_ref())?;
457        if key == "rdf:resource" {
458            return Ok(Some(strip_fragment(std::str::from_utf8(&attr.value)?)));
459        }
460    }
461    Ok(None)
462}