Skip to main content

cimdecoder/
lib.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use quick_xml::events::Event;
5use quick_xml::Reader;
6
7use cimstructs::base::{CimElement, FieldValue, RdfBlock};
8use cimstructs::registry::{self, ParseFn};
9
10pub struct CimEntry {
11    pub element: Box<dyn CimElement>,
12    pub block: RdfBlock,
13}
14
15pub struct CimDataset {
16    pub entries: HashMap<String, CimEntry>,
17    /// Maps `type_name()` → list of MRIDs of that type. Populated on insert, maintained on merge.
18    pub by_type: HashMap<String, Vec<String>>,
19}
20
21impl Default for CimDataset {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl CimDataset {
28    pub fn new() -> Self {
29        Self {
30            entries: HashMap::new(),
31            by_type: HashMap::new(),
32        }
33    }
34
35    /// Decode an RDF/XML string into a CimDataset, using the provided type registry.
36    pub fn decode_str(content: &str) -> Result<Self, Box<dyn std::error::Error>> {
37        let mut ds = Self::new();
38        let reg = registry::registry();
39        parse_rdf(content, &reg, &mut ds)?;
40        Ok(ds)
41    }
42
43    /// Decode an RDF/XML file into a CimDataset, using the provided type registry.
44    pub fn decode_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
45        Self::decode_str(&std::fs::read_to_string(path)?)
46    }
47
48    /// Decode multiple RDF/XML files into a single CimDataset, merging entries with the same MRID.
49    pub fn decode_files(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
50        let mut combined = Self::new();
51        for path in paths {
52            let file_ds = Self::decode_file(path)?;
53            combined.merge(file_ds);
54        }
55        Ok(combined)
56    }
57
58    /// Decode files in parallel using one thread per file, without merging.
59    /// Preserves input order. Falls back to sequential decoding for 0–1 paths
60    /// to avoid thread-spawn overhead.
61    pub fn decode_files_parallel_separate(paths: &[&Path]) -> Result<Vec<Self>, Box<dyn std::error::Error>> {
62        if paths.len() <= 1 {
63            return paths.iter().map(|p| Self::decode_file(p)).collect();
64        }
65        let results: Vec<Result<Self, String>> = std::thread::scope(|s| {
66            paths
67                .iter()
68                .map(|p| s.spawn(|| Self::decode_file(p).map_err(|e| e.to_string())))
69                .collect::<Vec<_>>()
70                .into_iter()
71                .map(|h| h.join().expect("decode thread panicked"))
72                .collect()
73        });
74        results
75            .into_iter()
76            .collect::<Result<_, String>>()
77            .map_err(|e| e.into())
78    }
79
80    /// Decode files in parallel using one thread per file, then merge sequentially.
81    pub fn decode_files_parallel(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
82        let datasets = Self::decode_files_parallel_separate(paths)?;
83        Ok(datasets
84            .into_iter()
85            .reduce(|mut a, b| {
86                a.merge(b);
87                a
88            })
89            .unwrap_or_default())
90    }
91
92    /// Merge another dataset into self, combining objects with the same MRID.
93    /// For conflicting MRIDs: merge RdfBlocks (later scalar wins, lists union),
94    /// then re-instantiate the typed element.
95    pub fn merge(&mut self, other: CimDataset) {
96        let reg = registry::registry();
97        for (mrid, incoming) in other.entries {
98            if let Some(existing) = self.entries.get_mut(&mrid) {
99                existing.block.merge_from(&incoming.block);
100                let type_name = existing.block.type_name.clone();
101                if let Some(f) = reg.get(type_name.as_str()) {
102                    existing.element = f(&existing.block);
103                }
104            } else {
105                let type_name = incoming.element.type_name().to_string();
106                self.by_type.entry(type_name).or_default().push(mrid.clone());
107                self.entries.insert(mrid, incoming);
108            }
109        }
110    }
111
112    /// Release all RdfBlocks to free memory after the final merge.
113    pub fn drop_blocks(&mut self) {
114        for entry in self.entries.values_mut() {
115            entry.block = RdfBlock::default();
116        }
117    }
118
119    /// Insert or replace the element at `mrid`, keeping `by_type` consistent
120    /// (including the case where `mrid` already exists under a different type).
121    pub fn set(&mut self, mrid: String, element: Box<dyn CimElement>) {
122        let new_type = element.type_name().to_string();
123        if let Some(old) = self.entries.get(&mrid) {
124            let old_type = old.element.type_name();
125            if old_type != new_type {
126                if let Some(v) = self.by_type.get_mut(old_type) {
127                    v.retain(|m| m != &mrid);
128                }
129            }
130        }
131        let already_indexed = self
132            .by_type
133            .get(&new_type)
134            .is_some_and(|v| v.contains(&mrid));
135        if !already_indexed {
136            self.by_type.entry(new_type).or_default().push(mrid.clone());
137        }
138        let block = element.to_block();
139        self.entries.insert(mrid, CimEntry { element, block });
140    }
141
142    /// Remove the element at `mrid`, pruning it from `by_type`. Returns the
143    /// removed entry, or `None` if `mrid` was not present.
144    pub fn remove(&mut self, mrid: &str) -> Option<CimEntry> {
145        let removed = self.entries.remove(mrid)?;
146        if let Some(v) = self.by_type.get_mut(removed.element.type_name()) {
147            v.retain(|m| m != mrid);
148        }
149        Some(removed)
150    }
151}
152
153// --- XML streaming parser ---------------------------------------------------
154
155/// Parse an RDF/XML string into a CimDataset, using the provided type registry.
156fn parse_rdf(
157    content: &str,
158    reg: &HashMap<&'static str, ParseFn>,
159    ds: &mut CimDataset,
160) -> Result<(), Box<dyn std::error::Error>> {
161    let mut reader = Reader::from_str(content);
162    let mut buf = Vec::new();
163
164    let mut depth: u32 = 0;
165    let mut current: Option<RdfBlock> = None;
166    let mut pending_key: Option<String> = None;
167
168    loop {
169        match reader.read_event_into(&mut buf) {
170            Ok(Event::Start(ref e)) => {
171                depth += 1;
172                let local = local_name(e.name().as_ref())?;
173
174                match depth {
175                    2 => {
176                        let mrid = extract_about(e.attributes())?;
177                        current = Some(RdfBlock {
178                            type_name: local,
179                            mrid,
180                            fields: HashMap::new(),
181                            duplicate_fields: std::collections::HashSet::new(),
182                        });
183                    }
184                    3 => {
185                        if let Some(ref mut block) = current {
186                            if let Some(res) = find_resource(e.attributes())? {
187                                add_field(block, &local, FieldValue::Resource(res));
188                            } else {
189                                pending_key = Some(local);
190                            }
191                        }
192                    }
193                    _ => {}
194                }
195            }
196
197            Ok(Event::Empty(ref e)) => {
198                let local = local_name(e.name().as_ref())?;
199                match depth {
200                    1 => {
201                        // Top-level self-closing element: <cim:Foo rdf:ID="x" />
202                        let mrid = extract_about(e.attributes())?;
203                        if !mrid.is_empty() {
204                            if let Some(f) = reg.get(local.as_str()) {
205                                let block = RdfBlock { type_name: local, mrid: mrid.clone(), fields: HashMap::new(), duplicate_fields: std::collections::HashSet::new() };
206                                let element = f(&block);
207                                let type_name = element.type_name().to_string();
208                                ds.by_type.entry(type_name).or_default().push(mrid.clone());
209                                ds.entries.insert(mrid, CimEntry { element, block });
210                            }
211                        }
212                    }
213                    2 => {
214                        // Self-closing field element within the current type block.
215                        if let Some(ref mut block) = current {
216                            if let Some(res) = find_resource(e.attributes())? {
217                                add_field(block, &local, FieldValue::Resource(res));
218                            }
219                        }
220                    }
221                    _ => {}
222                }
223            }
224
225            Ok(Event::Text(ref e)) => {
226                if depth == 3 {
227                    if let (Some(block), Some(key)) =
228                        (&mut current, pending_key.take())
229                    {
230                        let text = e.unescape()?.trim().to_string();
231                        if !text.is_empty() {
232                            add_field(block, &key, FieldValue::Text(text));
233                        }
234                    }
235                }
236            }
237
238            Ok(Event::End(_)) => {
239                if depth == 2 {
240                    pending_key = None;
241                    if let Some(block) = current.take() {
242                        if !block.mrid.is_empty() {
243                            if let Some(f) = reg.get(block.type_name.as_str()) {
244                                let element = f(&block);
245                                let type_name = element.type_name().to_string();
246                                ds.by_type.entry(type_name).or_default().push(block.mrid.clone());
247                                ds.entries.insert(block.mrid.clone(), CimEntry { element, block });
248                            }
249                        }
250                    }
251                }
252                depth = depth.saturating_sub(1);
253            }
254
255            Ok(Event::Eof) => break,
256            Err(e) => return Err(Box::new(e)),
257            _ => {}
258        }
259        buf.clear();
260    }
261
262    Ok(())
263}
264
265// --- helpers ----------------------------------------------------------------
266
267/// Extract the local name from a qualified XML name, e.g. "cim:Foo" → "Foo".
268fn local_name(raw: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
269    let s = std::str::from_utf8(raw)?;
270    Ok(s.find(':').map(|i| &s[i + 1..]).unwrap_or(s).to_string())
271}
272
273/// Strip the fragment from a URI, e.g. "http://example.com#foo" → "foo".
274fn strip_fragment(s: &str) -> String {
275    if let Some(i) = s.rfind('#') {
276        s[i + 1..].to_string()
277    } else {
278        s.trim_start_matches('#').to_string()
279    }
280}
281
282/// Extract the value of the "rdf:about" or "rdf:ID" attribute from an element.
283fn extract_about(
284    attrs: quick_xml::events::attributes::Attributes<'_>,
285) -> Result<String, Box<dyn std::error::Error>> {
286    for attr in attrs.flatten() {
287        let key = std::str::from_utf8(attr.key.as_ref())?;
288        if key == "rdf:about" || key == "rdf:ID" {
289            return Ok(strip_fragment(std::str::from_utf8(&attr.value)?));
290        }
291    }
292    Ok(String::new())
293}
294
295/// Find the value of the "rdf:resource" attribute from an element.
296fn find_resource(
297    attrs: quick_xml::events::attributes::Attributes<'_>,
298) -> Result<Option<String>, Box<dyn std::error::Error>> {
299    for attr in attrs.flatten() {
300        let key = std::str::from_utf8(attr.key.as_ref())?;
301        if key == "rdf:resource" {
302            return Ok(Some(strip_fragment(std::str::from_utf8(&attr.value)?)));
303        }
304    }
305    Ok(None)
306}
307
308/// Insert a field value, upgrading Resource → ResourceList and Text → TextList
309/// on repeated keys (e.g. the multiple md:Model.profile entries in a combined
310/// EQ+SC file header). Every repeated assignment is also recorded in
311/// `block.duplicate_fields` (the generated MaxCount=1 checks read that set).
312fn add_field(block: &mut RdfBlock, key: &str, val: FieldValue) {
313    match &val {
314        FieldValue::Resource(new_ref) => match block.fields.get_mut(key) {
315            Some(FieldValue::ResourceList(list)) => {
316                list.push(new_ref.clone());
317                block.duplicate_fields.insert(key.to_string());
318                return;
319            }
320            Some(existing @ FieldValue::Resource(_)) => {
321                let old = match existing {
322                    FieldValue::Resource(s) => s.clone(),
323                    _ => unreachable!(),
324                };
325                *existing = FieldValue::ResourceList(vec![old, new_ref.clone()]);
326                block.duplicate_fields.insert(key.to_string());
327                return;
328            }
329            _ => {}
330        },
331        FieldValue::Text(new_text) => match block.fields.get_mut(key) {
332            Some(FieldValue::TextList(list)) => {
333                list.push(new_text.clone());
334                block.duplicate_fields.insert(key.to_string());
335                return;
336            }
337            Some(existing @ FieldValue::Text(_)) => {
338                let old = match existing {
339                    FieldValue::Text(s) => s.clone(),
340                    _ => unreachable!(),
341                };
342                *existing = FieldValue::TextList(vec![old, new_text.clone()]);
343                block.duplicate_fields.insert(key.to_string());
344                return;
345            }
346            _ => {}
347        },
348        _ => {
349            if block.fields.contains_key(key) {
350                block.duplicate_fields.insert(key.to_string());
351            }
352        }
353    }
354    block.fields.insert(key.to_string(), val);
355}