1use std::collections::HashMap;
2use std::fs;
3
4use quick_xml::events::Event;
5use quick_xml::Reader;
6
7use super::family::Family;
8use super::model::*;
9
10#[derive(Debug, Default)]
12struct Block {
13 about: String,
14 rdf_types: Vec<String>,
15 label: String,
16 comment: String,
17 sub_class_of: String,
18 domain: String,
19 range: String,
20 stereotypes: Vec<String>,
23 concrete: bool,
25 multiplicity: String,
26 association_used: String,
27 inverse_role: String,
28 data_type: String,
29 is_fixed: String,
30 category: String,
31 version_info: String,
32 version_iri: String,
33 keyword: String,
34 title: String,
35}
36
37const CLASSIFIERS: &[&str] = &["enumeration", "Primitive", "CIMDatatype", "Compound"];
40
41impl Block {
42 fn classifier(&self, family: &Family) -> String {
50 if family.stereotype_first_wins {
51 if let Some(s) = self
52 .stereotypes
53 .iter()
54 .map(|s| uri_end(s))
55 .find(|s| CLASSIFIERS.contains(&s.as_str()))
56 {
57 return s;
58 }
59 }
60 self.stereotypes.last().map(|s| uri_end(s)).unwrap_or_default()
61 }
62
63 fn set(&mut self, tag: &str, value: String) {
64 match tag {
65 "rdf:type" => self.rdf_types.push(value),
66 "rdfs:label" => self.label = value,
67 "rdfs:comment" => self.comment = value,
68 "rdfs:subClassOf" => self.sub_class_of = value,
69 "rdfs:domain" => self.domain = value,
70 "rdfs:range" => self.range = value,
71 "cims:stereotype" => {
72 self.concrete |= uri_end(&value) == "concrete";
73 self.stereotypes.push(value);
74 }
75 "cims:multiplicity" => self.multiplicity = value,
76 "cims:AssociationUsed" => self.association_used = value,
77 "cims:inverseRoleName" => self.inverse_role = value,
78 "cims:dataType" => self.data_type = value,
79 "cims:isFixed" => self.is_fixed = value,
80 "cims:belongsToCategory" => self.category = value,
81 "owl:versionInfo" => self.version_info = value,
82 "owl:versionIRI" => self.version_iri = value,
83 "dcat:keyword" => self.keyword = value,
84 "dcterms:title" => self.title = value,
85 _ => {}
86 }
87 }
88}
89
90pub fn import_schema_files(
91 pattern: &str,
92 family: &'static Family,
93 verbose: bool,
94) -> Result<CimSpecification, Box<dyn std::error::Error>> {
95 let mut paths: Vec<String> = glob::glob(pattern)?
96 .filter_map(Result::ok)
97 .map(|p| p.to_string_lossy().into_owned())
98 .collect();
99
100 if paths.is_empty() {
101 return Err(format!("no schema files matched: {pattern}").into());
102 }
103 paths.sort();
104
105 let mut spec = CimSpecification::new(family);
106 for path in &paths {
107 if verbose {
108 eprintln!("parsing {path}");
109 }
110 parse_file(path, family, &mut spec)?;
111 }
112
113 super::processing::postprocess(&mut spec);
114 Ok(spec)
115}
116
117fn parse_file(
118 path: &str,
119 family: &'static Family,
120 spec: &mut CimSpecification,
121) -> Result<(), Box<dyn std::error::Error>> {
122 let content = fs::read_to_string(path)
123 .map_err(|e| format!("cannot read {path}: {e}"))?;
124
125 let mut reader = Reader::from_str(&content);
126 let mut buf = Vec::new();
127
128 let mut ns_map: HashMap<String, String> = HashMap::new();
129 let mut blocks: Vec<Block> = Vec::new();
130 let mut current: Option<Block> = None;
131 let mut current_tag: Option<String> = None;
132
133 loop {
134 match reader.read_event_into(&mut buf) {
135 Ok(Event::Start(ref e)) => {
136 let name = bytes_to_str(e.name().as_ref())?;
137
138 if name == "rdf:RDF" {
139 for attr in e.attributes().flatten() {
140 let key = bytes_to_str(attr.key.as_ref())?;
141 let val = std::str::from_utf8(&attr.value)?.to_string();
142 if let Some(prefix) = key.strip_prefix("xmlns:").or_else(|| key.strip_prefix("xml:")) {
143 let ns = ensure_terminated(val);
144 ns_map.insert(prefix.to_string(), ns);
145 }
146 }
147 } else if name == "rdf:Description" {
148 let mut b = Block::default();
149 for attr in e.attributes().flatten() {
150 let key = bytes_to_str(attr.key.as_ref())?;
151 if key == "rdf:about" {
152 b.about = std::str::from_utf8(&attr.value)?.to_string();
153 }
154 }
155 current = Some(b);
156 } else if current.is_some() {
157 let resource = find_resource(e.attributes());
158 if let Some(res) = resource {
159 if let Some(ref mut b) = current {
160 b.set(&name, res);
161 }
162 current_tag = None;
163 } else {
164 current_tag = Some(name.to_string());
165 }
166 }
167 }
168
169 Ok(Event::Empty(ref e)) => {
170 let name = bytes_to_str(e.name().as_ref())?;
171
172 if name == "rdf:Description" {
173 let mut b = Block::default();
174 for attr in e.attributes().flatten() {
175 let key = bytes_to_str(attr.key.as_ref())?;
176 if key == "rdf:about" {
177 b.about = std::str::from_utf8(&attr.value)?.to_string();
178 }
179 }
180 blocks.push(b);
181 } else if current.is_some() {
182 if let Some(res) = find_resource(e.attributes()) {
183 if let Some(ref mut b) = current {
184 b.set(&name, res);
185 }
186 }
187 }
188 }
189
190 Ok(Event::Text(ref e)) => {
191 if let Some(tag) = current_tag.take() {
192 let text = e.unescape()?.into_owned();
193 let text = text.trim().to_string();
194 if !text.is_empty() {
195 if let Some(ref mut b) = current {
196 b.set(&tag, text);
197 }
198 }
199 }
200 }
201
202 Ok(Event::End(ref e)) => {
203 let name = bytes_to_str(e.name().as_ref())?;
204 if name == "rdf:Description" {
205 if let Some(b) = current.take() {
206 blocks.push(b);
207 }
208 }
209 current_tag = None;
210 }
211
212 Ok(Event::Eof) => break,
213 Err(e) => return Err(Box::new(e)),
214 _ => {}
215 }
216 buf.clear();
217 }
218
219 let keyword = blocks
221 .iter()
222 .find(|b| b.rdf_types.iter().any(|t| t.contains("owl#Ontology")))
223 .map(|b| b.keyword.clone())
224 .unwrap_or_default();
225
226 let mut types: HashMap<String, CimType> = HashMap::new();
228 let mut enums: HashMap<String, CimEnum> = HashMap::new();
229 let mut datatypes: HashMap<String, CimDatatype> = HashMap::new();
230 let mut primitives: HashMap<String, CimPrimitive> = HashMap::new();
231 let mut attributes: Vec<CimAttribute> = Vec::new();
232 let mut enum_values: Vec<CimEnumValue> = Vec::new();
233 let mut ontology = CimOntology::default();
234
235 for b in blocks {
236 let type_str = b.rdf_types.join(" ");
237 if type_str.contains("rdf-schema#Class") {
238 let stereo = b.classifier(family);
239 if stereo == "enumeration" {
240 let mut e = make_enum(&b, family);
241 e.origin = keyword.clone();
242 e.origins = vec![keyword.clone()];
243 enums.insert(e.id.clone(), e);
244 } else if stereo == "CIMDatatype" {
245 let d = make_datatype(&b, family);
246 datatypes.insert(d.id.clone(), d);
247 } else if stereo == "Primitive" {
248 let p = make_primitive(&b, family);
249 primitives.insert(p.id.clone(), p);
250 } else {
251 let mut t = make_type(&b, family);
252 t.origin = keyword.clone();
253 t.origins = vec![keyword.clone()];
254 if b.concrete {
255 t.concrete_in = vec![keyword.clone()];
256 }
257 types.insert(t.id.clone(), t);
258 }
259 } else if type_str.contains("rdf-syntax-ns#Property") {
260 let mut attr = make_attribute(&b, family);
261 attr.origin = keyword.clone();
262 attr.origins = vec![keyword.clone()];
263 attributes.push(attr);
264 } else if type_str.contains("owl#Ontology") {
265 ontology = make_ontology(&b);
266 } else if type_str.contains("ClassCategory") {
267 } else if !type_str.is_empty() {
269 enum_values.push(make_enum_value(&b, family));
271 }
272 }
273
274 assign_attrs_to_types(&mut types, &attributes);
275 assign_attrs_to_datatypes(&mut datatypes, &attributes);
276 assign_values_to_enums(&mut enums, &enum_values);
277
278 merge_types(&mut spec.types, types);
279 merge_enums(&mut spec.enums, enums);
280 merge_datatypes(&mut spec.cim_datatypes, datatypes);
281 merge_primitives(&mut spec.primitive_types, primitives);
282
283 if !ontology.keyword.is_empty() {
284 spec.ontologies.insert(ontology.keyword.clone(), ontology);
285 }
286
287 for (k, v) in ns_map {
288 spec.specification_namespaces.entry(k).or_insert(v);
289 }
290
291 Ok(())
292}
293
294fn make_type(b: &Block, family: &Family) -> CimType {
297 CimType {
298 id: uri_end(&b.about),
299 label: b.label.clone(),
300 comment: clean_text(&b.comment),
301 namespace: uri_path(&b.about),
302 super_type: uri_end(&b.sub_class_of),
303 cim_stereotype: b.classifier(family),
304 rdf_type: b.rdf_types.first().map(|s| uri_end(s)).unwrap_or_default(),
305 cim_categories: vec![uri_end(&b.category)],
306 ..Default::default()
307 }
308}
309
310fn make_primitive(b: &Block, family: &Family) -> CimPrimitive {
311 CimPrimitive {
312 id: uri_end(&b.about),
313 label: b.label.clone(),
314 comment: clean_text(&b.comment),
315 namespace: uri_path(&b.about),
316 cim_stereotype: b.classifier(family),
317 rdf_type: b.rdf_types.first().map(|s| uri_end(s)).unwrap_or_default(),
318 ..Default::default()
319 }
320}
321
322fn make_datatype(b: &Block, family: &Family) -> CimDatatype {
323 CimDatatype {
324 id: uri_end(&b.about),
325 label: b.label.clone(),
326 comment: clean_text(&b.comment),
327 namespace: uri_path(&b.about),
328 cim_stereotype: b.classifier(family),
329 rdf_type: b.rdf_types.first().map(|s| uri_end(s)).unwrap_or_default(),
330 cim_category: uri_end(&b.category),
331 ..Default::default()
332 }
333}
334
335fn make_enum(b: &Block, family: &Family) -> CimEnum {
336 CimEnum {
337 id: uri_end(&b.about),
338 label: b.label.clone(),
339 comment: clean_text(&b.comment),
340 namespace: uri_path(&b.about),
341 cim_stereotype: b.classifier(family),
342 rdf_type: b.rdf_types.first().map(|s| uri_end(s)).unwrap_or_default(),
343 ..Default::default()
344 }
345}
346
347fn make_enum_value(b: &Block, family: &Family) -> CimEnumValue {
348 CimEnumValue {
349 id: uri_end(&b.about),
350 label: b.label.clone(),
351 comment: clean_text(&b.comment),
352 cim_stereotype: b.classifier(family),
353 rdf_type: b.rdf_types.first().map(|s| uri_end(s)).unwrap_or_default(),
354 }
355}
356
357fn make_ontology(b: &Block) -> CimOntology {
358 CimOntology {
359 id: uri_end(&b.about),
360 namespace: uri_path(&b.about),
361 rdf_type: b.rdf_types.first().map(|s| uri_end(s)).unwrap_or_default(),
362 owl_version_iri: b.version_iri.clone(),
363 owl_version_info: b.version_info.clone(),
364 keyword: b.keyword.clone(),
365 name: b.title.trim_end_matches(" Vocabulary").to_string(),
366 priority: 0,
367 }
368}
369
370fn make_attribute(b: &Block, family: &Family) -> CimAttribute {
371 let assoc = b.association_used.to_lowercase();
372 let is_assoc = assoc == "yes" || assoc.is_empty();
373 let is_list = is_list_multiplicity(&b.multiplicity);
374 CimAttribute {
375 id: uri_end(&b.about),
376 label: b.label.clone(),
377 comment: clean_text(&b.comment),
378 namespace: uri_path(&b.about),
379 cim_stereotype: b.classifier(family),
380 rdf_domain: uri_end(&b.domain),
381 rdf_range: uri_end(&b.range),
382 cim_data_type: uri_end(&b.data_type),
383 rdf_type: b.rdf_types.first().map(|s| uri_end(s)).unwrap_or_default(),
384 cim_association_used: assoc,
385 is_association_used: is_assoc,
386 cim_inverse_role: uri_end(&b.inverse_role),
387 cim_multiplicity: b.multiplicity.clone(),
388 is_list,
389 cim_is_fixed: b.is_fixed.clone(),
390 ..Default::default()
391 }
392}
393
394fn assign_attrs_to_types(types: &mut HashMap<String, CimType>, attrs: &[CimAttribute]) {
397 for attr in attrs {
398 if let Some(t) = types.get_mut(&attr.rdf_domain) {
399 let mut a = attr.clone();
400 a.cim_categories = t.cim_categories.clone();
401 t.attributes.push(a);
402 }
403 }
404}
405
406fn assign_attrs_to_datatypes(dt: &mut HashMap<String, CimDatatype>, attrs: &[CimAttribute]) {
407 for attr in attrs {
408 if let Some(d) = dt.get_mut(&attr.rdf_domain) {
409 d.attributes.push(attr.clone());
410 }
411 }
412}
413
414fn assign_values_to_enums(enums: &mut HashMap<String, CimEnum>, values: &[CimEnumValue]) {
415 for val in values {
416 if let Some(e) = enums.get_mut(&val.rdf_type) {
417 e.values.push(val.clone());
418 }
419 }
420}
421
422fn merge_types(target: &mut HashMap<String, CimType>, source: HashMap<String, CimType>) {
425 for (k, src) in source {
426 if let Some(existing) = target.get_mut(&k) {
427 if !src.super_type.is_empty() {
428 existing.super_type = src.super_type;
429 }
430 if !src.cim_stereotype.is_empty() {
431 existing.cim_stereotype = src.cim_stereotype;
432 }
433 if existing.comment.is_empty() {
436 existing.comment = src.comment;
437 }
438 existing.cim_categories.extend(src.cim_categories);
439 if !src.origin.is_empty() {
440 existing.origins.push(src.origin);
441 }
442 existing.concrete_in.extend(src.concrete_in);
443 for attr in src.attributes {
444 if let Some(idx) = existing.attributes.iter().position(|a| a.id == attr.id) {
445 if existing.attributes[idx].comment.is_empty() {
446 existing.attributes[idx].comment = attr.comment;
447 }
448 existing.attributes[idx].origins.extend(attr.origins);
449 existing.attributes[idx]
450 .cim_categories
451 .extend(attr.cim_categories);
452 } else {
453 existing.attributes.push(attr);
454 }
455 }
456 } else {
457 target.insert(k, src);
458 }
459 }
460}
461
462fn merge_enums(target: &mut HashMap<String, CimEnum>, source: HashMap<String, CimEnum>) {
463 for (k, src) in source {
464 if let Some(existing) = target.get_mut(&k) {
465 if !src.cim_stereotype.is_empty() {
466 existing.cim_stereotype = src.cim_stereotype;
467 }
468 if existing.comment.is_empty() {
469 existing.comment = src.comment;
470 }
471 if !src.origin.is_empty() {
472 existing.origins.push(src.origin);
473 }
474 for val in src.values {
475 match existing.values.iter_mut().find(|v| v.id == val.id) {
476 Some(v) if v.comment.is_empty() => v.comment = val.comment,
477 Some(_) => {}
478 None => existing.values.push(val),
479 }
480 }
481 } else {
482 target.insert(k, src);
483 }
484 }
485}
486
487fn merge_datatypes(
488 target: &mut HashMap<String, CimDatatype>,
489 source: HashMap<String, CimDatatype>,
490) {
491 for (k, src) in source {
492 if let Some(existing) = target.get_mut(&k) {
493 if !src.cim_stereotype.is_empty() {
494 existing.cim_stereotype = src.cim_stereotype;
495 }
496 } else {
497 target.insert(k, src);
498 }
499 }
500}
501
502fn merge_primitives(
503 target: &mut HashMap<String, CimPrimitive>,
504 source: HashMap<String, CimPrimitive>,
505) {
506 for (k, src) in source {
507 if let Some(existing) = target.get_mut(&k) {
508 if !src.cim_stereotype.is_empty() {
509 existing.cim_stereotype = src.cim_stereotype;
510 }
511 } else {
512 target.insert(k, src);
513 }
514 }
515}
516
517fn bytes_to_str(b: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
520 Ok(std::str::from_utf8(b)?.to_string())
521}
522
523fn find_resource(
524 attrs: quick_xml::events::attributes::Attributes,
525) -> Option<String> {
526 for attr in attrs.flatten() {
527 if let Ok(key) = std::str::from_utf8(attr.key.as_ref()) {
528 if key == "rdf:resource" {
529 if let Ok(s) = std::str::from_utf8(&attr.value) {
530 return Some(s.to_string());
531 }
532 }
533 }
534 }
535 None
536}
537
538pub fn uri_end(uri: &str) -> String {
542 if let Some(pos) = uri.rfind('#') {
543 uri[pos + 1..].to_string()
544 } else if let Some(pos) = uri.rfind('/') {
545 uri[pos + 1..].to_string()
546 } else {
547 uri.to_string()
548 }
549}
550
551pub fn uri_path(uri: &str) -> String {
553 if let Some(pos) = uri.rfind('#') {
554 uri[..=pos].to_string()
555 } else if let Some(pos) = uri.rfind('/') {
556 uri[..=pos].to_string()
557 } else {
558 String::new()
559 }
560}
561
562fn is_list_multiplicity(m: &str) -> bool {
568 let token = match m.rfind("#M:") {
569 Some(pos) => &m[pos + 3..],
570 None => return false,
571 };
572 let upper = token.rsplit("..").next().unwrap_or(token);
573 upper == "n" || upper.parse::<u32>().is_ok_and(|n| n > 1)
574}
575
576fn ensure_terminated(mut ns: String) -> String {
579 if !ns.ends_with('#') && !ns.ends_with('/') {
580 ns.push('#');
581 }
582 ns
583}
584
585pub fn clean_text(s: &str) -> String {
586 let mut out = String::with_capacity(s.len());
588 let mut in_tag = false;
589 for ch in s.chars() {
590 match ch {
591 '<' => in_tag = true,
592 '>' => in_tag = false,
593 _ if !in_tag => out.push(ch),
594 _ => {}
595 }
596 }
597 let out = out
598 .replace("&", "&")
599 .replace("<", "<")
600 .replace(">", ">")
601 .replace(""", "'")
602 .replace("'", "'")
603 .replace('"', "'")
604 .replace('\u{2013}', "-"); out.split_whitespace().collect::<Vec<_>>().join(" ")
607}
608
609#[derive(Debug)]
616pub struct ProfileInfo {
617 pub keyword: String,
619 pub iris: Vec<String>,
622}
623
624pub fn import_profile_index(
631 dir: &std::path::Path,
632) -> Result<Vec<ProfileInfo>, Box<dyn std::error::Error>> {
633 let mut paths: Vec<std::path::PathBuf> = std::fs::read_dir(dir)?
634 .filter_map(|e| e.ok())
635 .map(|e| e.path())
636 .filter(|p| p.extension().and_then(|x| x.to_str()) == Some("rdf"))
637 .collect();
638 paths.sort();
639
640 let mut out = Vec::new();
641 for path in &paths {
642 if let Some(info) = read_profile(path)? {
643 out.push(info);
644 }
645 }
646 out.sort_by(|a, b| a.keyword.cmp(&b.keyword));
647 Ok(out)
648}
649
650fn read_profile(
651 path: &std::path::Path,
652) -> Result<Option<ProfileInfo>, Box<dyn std::error::Error>> {
653 use quick_xml::events::Event;
654
655 let mut reader = quick_xml::Reader::from_file(path)?;
656 reader.config_mut().trim_text(true);
657
658 let mut buf = Vec::new();
659 let mut keyword: Option<String> = None;
660 let mut current: Option<String> = None;
665 let mut subject: Option<String> = None;
666 let mut iris: Vec<String> = Vec::new();
667 let mut versions: Vec<(String, String)> = Vec::new();
668 let mut in_keyword = false;
669 let mut depth = 0usize;
670
671 loop {
672 match reader.read_event_into(&mut buf)? {
673 Event::Eof => break,
674 Event::Start(e) | Event::Empty(e) => {
675 let name = e.name();
676 let local = local_name(name.as_ref());
677 match local {
678 b"Description" => {
679 depth += 1;
680 if depth == 1 {
681 current = attr(&e, b"about")?;
682 }
683 }
684 b"keyword" if depth == 1 => in_keyword = true,
685 b"versionIRI" if depth == 1 => {
689 if let (Some(subj), Some(v)) = (current.clone(), attr(&e, b"resource")?) {
690 versions.push((subj, v));
691 }
692 }
693 _ => {}
694 }
695 }
696 Event::End(e) => {
697 let name = e.name();
698 match local_name(name.as_ref()) {
699 b"Description" => depth = depth.saturating_sub(1),
700 b"keyword" => in_keyword = false,
701 _ => {}
702 }
703 }
704 Event::Text(t) if in_keyword => {
705 let s = t.unescape()?.trim().to_string();
706 if !s.is_empty() && keyword.is_none() {
707 keyword = Some(s);
708 subject = current.clone();
709 }
710 }
711 _ => {}
712 }
713 buf.clear();
714 }
715
716 let Some(keyword) = keyword else { return Ok(None) };
717 let Some(subject) = subject else { return Ok(None) };
718 iris.extend(
719 versions
720 .into_iter()
721 .filter(|(subj, _)| *subj == subject)
722 .map(|(_, v)| v),
723 );
724 iris.push(subject);
725 iris.sort();
726 iris.dedup();
727 if iris.is_empty() {
728 return Ok(None);
729 }
730 Ok(Some(ProfileInfo { keyword, iris }))
731}
732
733fn local_name(qname: &[u8]) -> &[u8] {
734 match qname.iter().position(|b| *b == b':') {
735 Some(i) => &qname[i + 1..],
736 None => qname,
737 }
738}
739
740fn attr(
741 e: &quick_xml::events::BytesStart<'_>,
742 want: &[u8],
743) -> Result<Option<String>, Box<dyn std::error::Error>> {
744 for a in e.attributes().flatten() {
745 if local_name(a.key.as_ref()) == want {
746 return Ok(Some(String::from_utf8(a.value.to_vec())?));
747 }
748 }
749 Ok(None)
750}