use std::collections::HashMap;
use std::path::Path;
use quick_xml::events::Event;
use quick_xml::Reader;
use cimstructs::base::{CimElement, FieldValue, RdfBlock};
use cimstructs::registry::{self, ParseFn};
pub struct CimEntry {
pub element: Box<dyn CimElement>,
pub block: RdfBlock,
}
pub struct CimDataset {
pub entries: HashMap<String, CimEntry>,
pub by_type: HashMap<String, Vec<String>>,
}
impl Default for CimDataset {
fn default() -> Self {
Self::new()
}
}
impl CimDataset {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
by_type: HashMap::new(),
}
}
pub fn decode_str(content: &str) -> Result<Self, Box<dyn std::error::Error>> {
let mut ds = Self::new();
let reg = registry::registry();
parse_rdf(content, ®, &mut ds)?;
Ok(ds)
}
pub fn decode_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
Self::decode_str(&std::fs::read_to_string(path)?)
}
pub fn decode_files(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
let mut combined = Self::new();
for path in paths {
let file_ds = Self::decode_file(path)?;
combined.merge(file_ds);
}
Ok(combined)
}
pub fn decode_files_parallel_separate(paths: &[&Path]) -> Result<Vec<Self>, Box<dyn std::error::Error>> {
if paths.len() <= 1 {
return paths.iter().map(|p| Self::decode_file(p)).collect();
}
let results: Vec<Result<Self, String>> = std::thread::scope(|s| {
paths
.iter()
.map(|p| s.spawn(|| Self::decode_file(p).map_err(|e| e.to_string())))
.collect::<Vec<_>>()
.into_iter()
.map(|h| h.join().expect("decode thread panicked"))
.collect()
});
results
.into_iter()
.collect::<Result<_, String>>()
.map_err(|e| e.into())
}
pub fn decode_files_parallel(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
let datasets = Self::decode_files_parallel_separate(paths)?;
Ok(datasets
.into_iter()
.reduce(|mut a, b| {
a.merge(b);
a
})
.unwrap_or_default())
}
pub fn merge(&mut self, other: CimDataset) {
let reg = registry::registry();
for (mrid, incoming) in other.entries {
if let Some(existing) = self.entries.get_mut(&mrid) {
existing.block.merge_from(&incoming.block);
let type_name = existing.block.type_name.clone();
if let Some(f) = reg.get(type_name.as_str()) {
existing.element = f(&existing.block);
}
} else {
let type_name = incoming.element.type_name().to_string();
self.by_type.entry(type_name).or_default().push(mrid.clone());
self.entries.insert(mrid, incoming);
}
}
}
pub fn drop_blocks(&mut self) {
for entry in self.entries.values_mut() {
entry.block = RdfBlock::default();
}
}
pub fn set(&mut self, mrid: String, element: Box<dyn CimElement>) {
let new_type = element.type_name().to_string();
if let Some(old) = self.entries.get(&mrid) {
let old_type = old.element.type_name();
if old_type != new_type {
if let Some(v) = self.by_type.get_mut(old_type) {
v.retain(|m| m != &mrid);
}
}
}
let already_indexed = self
.by_type
.get(&new_type)
.is_some_and(|v| v.contains(&mrid));
if !already_indexed {
self.by_type.entry(new_type).or_default().push(mrid.clone());
}
let block = element.to_block();
self.entries.insert(mrid, CimEntry { element, block });
}
pub fn remove(&mut self, mrid: &str) -> Option<CimEntry> {
let removed = self.entries.remove(mrid)?;
if let Some(v) = self.by_type.get_mut(removed.element.type_name()) {
v.retain(|m| m != mrid);
}
Some(removed)
}
}
fn parse_rdf(
content: &str,
reg: &HashMap<&'static str, ParseFn>,
ds: &mut CimDataset,
) -> Result<(), Box<dyn std::error::Error>> {
let mut reader = Reader::from_str(content);
let mut buf = Vec::new();
let mut depth: u32 = 0;
let mut current: Option<RdfBlock> = None;
let mut pending_key: Option<String> = None;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
depth += 1;
let local = local_name(e.name().as_ref())?;
match depth {
2 => {
let mrid = extract_about(e.attributes())?;
current = Some(RdfBlock {
type_name: local,
mrid,
fields: HashMap::new(),
duplicate_fields: std::collections::HashSet::new(),
});
}
3 => {
if let Some(ref mut block) = current {
if let Some(res) = find_resource(e.attributes())? {
add_field(block, &local, FieldValue::Resource(res));
} else {
pending_key = Some(local);
}
}
}
_ => {}
}
}
Ok(Event::Empty(ref e)) => {
let local = local_name(e.name().as_ref())?;
match depth {
1 => {
let mrid = extract_about(e.attributes())?;
if !mrid.is_empty() {
if let Some(f) = reg.get(local.as_str()) {
let block = RdfBlock { type_name: local, mrid: mrid.clone(), fields: HashMap::new(), duplicate_fields: std::collections::HashSet::new() };
let element = f(&block);
let type_name = element.type_name().to_string();
ds.by_type.entry(type_name).or_default().push(mrid.clone());
ds.entries.insert(mrid, CimEntry { element, block });
}
}
}
2 => {
if let Some(ref mut block) = current {
if let Some(res) = find_resource(e.attributes())? {
add_field(block, &local, FieldValue::Resource(res));
}
}
}
_ => {}
}
}
Ok(Event::Text(ref e)) => {
if depth == 3 {
if let (Some(block), Some(key)) =
(&mut current, pending_key.take())
{
let text = e.unescape()?.trim().to_string();
if !text.is_empty() {
add_field(block, &key, FieldValue::Text(text));
}
}
}
}
Ok(Event::End(_)) => {
if depth == 2 {
pending_key = None;
if let Some(block) = current.take() {
if !block.mrid.is_empty() {
if let Some(f) = reg.get(block.type_name.as_str()) {
let element = f(&block);
let type_name = element.type_name().to_string();
ds.by_type.entry(type_name).or_default().push(block.mrid.clone());
ds.entries.insert(block.mrid.clone(), CimEntry { element, block });
}
}
}
}
depth = depth.saturating_sub(1);
}
Ok(Event::Eof) => break,
Err(e) => return Err(Box::new(e)),
_ => {}
}
buf.clear();
}
Ok(())
}
fn local_name(raw: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
let s = std::str::from_utf8(raw)?;
Ok(s.find(':').map(|i| &s[i + 1..]).unwrap_or(s).to_string())
}
fn strip_fragment(s: &str) -> String {
if let Some(i) = s.rfind('#') {
s[i + 1..].to_string()
} else {
s.trim_start_matches('#').to_string()
}
}
fn extract_about(
attrs: quick_xml::events::attributes::Attributes<'_>,
) -> Result<String, Box<dyn std::error::Error>> {
for attr in attrs.flatten() {
let key = std::str::from_utf8(attr.key.as_ref())?;
if key == "rdf:about" || key == "rdf:ID" {
return Ok(strip_fragment(std::str::from_utf8(&attr.value)?));
}
}
Ok(String::new())
}
fn find_resource(
attrs: quick_xml::events::attributes::Attributes<'_>,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
for attr in attrs.flatten() {
let key = std::str::from_utf8(attr.key.as_ref())?;
if key == "rdf:resource" {
return Ok(Some(strip_fragment(std::str::from_utf8(&attr.value)?)));
}
}
Ok(None)
}
fn add_field(block: &mut RdfBlock, key: &str, val: FieldValue) {
match &val {
FieldValue::Resource(new_ref) => match block.fields.get_mut(key) {
Some(FieldValue::ResourceList(list)) => {
list.push(new_ref.clone());
block.duplicate_fields.insert(key.to_string());
return;
}
Some(existing @ FieldValue::Resource(_)) => {
let old = match existing {
FieldValue::Resource(s) => s.clone(),
_ => unreachable!(),
};
*existing = FieldValue::ResourceList(vec![old, new_ref.clone()]);
block.duplicate_fields.insert(key.to_string());
return;
}
_ => {}
},
FieldValue::Text(new_text) => match block.fields.get_mut(key) {
Some(FieldValue::TextList(list)) => {
list.push(new_text.clone());
block.duplicate_fields.insert(key.to_string());
return;
}
Some(existing @ FieldValue::Text(_)) => {
let old = match existing {
FieldValue::Text(s) => s.clone(),
_ => unreachable!(),
};
*existing = FieldValue::TextList(vec![old, new_text.clone()]);
block.duplicate_fields.insert(key.to_string());
return;
}
_ => {}
},
_ => {
if block.fields.contains_key(key) {
block.duplicate_fields.insert(key.to_string());
}
}
}
block.fields.insert(key.to_string(), val);
}