1use std::collections::{HashMap, HashSet};
4use std::path::PathBuf;
5
6use crate::data::HDict;
7use crate::kinds::Kind;
8use crate::xeto::Spec;
9
10use super::OntologyError;
11use super::conjunct::ConjunctIndex;
12use super::def::{Def, DefKind};
13use super::lib::Lib;
14use super::taxonomy::TaxonomyTree;
15use super::trio_loader::load_trio;
16use super::validation::{FitIssue, ValidationIssue};
17
18#[derive(Debug, Clone)]
20pub enum LibSource {
21 Bundled,
23 Trio(String),
25 Xeto(String),
27 Directory(PathBuf),
29}
30
31pub struct DefNamespace {
36 defs: HashMap<String, Def>,
38 libs: HashMap<String, Lib>,
40 taxonomy: TaxonomyTree,
42 conjuncts: ConjunctIndex,
44 mandatory_defs: HashSet<String>,
46 tag_on_index: HashMap<String, Vec<String>>,
48 choice_index: HashMap<String, Vec<String>>,
50 specs: HashMap<String, Spec>,
52 spec_libs: HashMap<String, Vec<String>>,
54 lib_sources: HashMap<String, LibSource>,
56}
57
58impl DefNamespace {
59 pub fn new() -> Self {
61 Self {
62 defs: HashMap::new(),
63 libs: HashMap::new(),
64 taxonomy: TaxonomyTree::new(),
65 conjuncts: ConjunctIndex::new(),
66 mandatory_defs: HashSet::new(),
67 tag_on_index: HashMap::new(),
68 choice_index: HashMap::new(),
69 specs: HashMap::new(),
70 spec_libs: HashMap::new(),
71 lib_sources: HashMap::new(),
72 }
73 }
74
75 pub fn load_standard() -> Result<Self, OntologyError> {
80 let source = include_str!("../../data/defs.trio");
81 let mut ns = Self::new();
82 let libs = load_trio(source)?;
83 for lib in libs {
84 let lib_name = lib.name.clone();
85 ns.register_lib(lib);
86 ns.set_lib_source(&lib_name, LibSource::Bundled);
87 }
88
89 for bundled in crate::xeto::bundled::bundled_libs() {
92 match crate::xeto::loader::load_xeto_source(bundled.source, bundled.name, &ns) {
93 Ok((lib, specs)) => {
94 if !ns.libs().contains_key(lib.name.as_str()) {
98 ns.register_lib(lib);
99 }
100 for spec in specs {
101 ns.register_spec(spec);
102 }
103 ns.set_lib_source(bundled.name, LibSource::Bundled);
104 }
105 Err(_e) => {
106 }
110 }
111 }
112
113 Ok(ns)
114 }
115
116 pub fn load_trio_str(&mut self, source: &str) -> Result<Vec<Lib>, OntologyError> {
118 let libs = load_trio(source)?;
119 for lib in &libs {
120 self.register_lib(lib.clone());
121 }
122 Ok(libs)
123 }
124
125 pub fn register_lib(&mut self, lib: Lib) {
131 let defs: Vec<Def> = lib.defs.values().cloned().collect();
132 self.libs.insert(lib.name.clone(), lib);
133
134 let mut new_symbols: Vec<String> = Vec::new();
136 for def in defs {
137 let symbol = def.symbol.clone();
138 new_symbols.push(symbol.clone());
139 self.register_def_basic(def);
140 }
141
142 for symbol in &new_symbols {
144 self.register_def_choice_index(symbol);
145 }
146 }
147
148 fn register_def_basic(&mut self, def: Def) {
151 let symbol = def.symbol.clone();
152
153 self.taxonomy.add(&symbol, &def.is_);
155
156 if def.mandatory {
158 self.mandatory_defs.insert(symbol.clone());
159 }
160
161 if symbol.contains('-') {
163 let parts: Vec<String> = symbol.split('-').map(|s| s.to_string()).collect();
164 self.conjuncts.register(&symbol, parts);
165 }
166
167 for target in &def.tag_on {
169 self.tag_on_index
170 .entry(target.clone())
171 .or_default()
172 .push(symbol.clone());
173 }
174
175 self.defs.insert(symbol, def);
177 }
178
179 fn register_def_choice_index(&mut self, symbol: &str) {
184 let is_ = match self.defs.get(symbol) {
185 Some(def) => def.is_.clone(),
186 None => return,
187 };
188
189 for parent in &is_ {
190 if let Some(parent_def) = self.defs.get(parent)
191 && parent_def.kind() == DefKind::Choice
192 {
193 self.choice_index
194 .entry(parent.clone())
195 .or_default()
196 .push(symbol.to_string());
197 }
198 }
199 }
200
201 pub fn get_def(&self, symbol: &str) -> Option<&Def> {
205 self.defs.get(symbol)
206 }
207
208 pub fn resolve(&self, name: &str) -> Option<&Def> {
210 self.get_def(name)
212 }
213
214 pub fn is_a(&self, name: &str, supertype: &str) -> bool {
220 self.taxonomy.is_subtype(name, supertype)
221 }
222
223 pub fn subtypes(&self, name: &str) -> Vec<String> {
225 self.taxonomy.subtypes_of(name)
226 }
227
228 pub fn supertypes(&self, name: &str) -> Vec<String> {
230 self.taxonomy.supertypes_of(name)
231 }
232
233 pub fn mandatory_tags(&self, name: &str) -> HashSet<String> {
237 self.taxonomy.mandatory_tags(name, &self.mandatory_defs)
238 }
239
240 pub fn tags_for(&self, name: &str) -> HashSet<String> {
242 let mut tags: HashSet<String> = HashSet::new();
243 if let Some(tag_list) = self.tag_on_index.get(name) {
245 tags.extend(tag_list.iter().cloned());
246 }
247 for sup in self.taxonomy.supertypes_of(name) {
249 if let Some(tag_list) = self.tag_on_index.get(&sup) {
250 tags.extend(tag_list.iter().cloned());
251 }
252 }
253 tags
254 }
255
256 pub fn conjunct_parts(&self, name: &str) -> Option<&[String]> {
258 self.conjuncts.decompose(name)
259 }
260
261 pub fn choices(&self, choice_name: &str) -> Vec<String> {
263 let choice_def = match self.defs.get(choice_name) {
264 Some(d) => d,
265 None => return vec![],
266 };
267 if let Some(ref of_target) = choice_def.of {
269 return self.taxonomy.all_subtypes(of_target);
270 }
271 self.choice_index
273 .get(choice_name)
274 .cloned()
275 .unwrap_or_default()
276 }
277
278 pub fn fits(&self, entity: &HDict, type_name: &str) -> bool {
285 let mandatory = self.mandatory_tags(type_name);
286 mandatory.iter().all(|tag| entity.has(tag))
287 }
288
289 pub fn fits_explain(&self, entity: &HDict, type_name: &str) -> Vec<FitIssue> {
293 let mut issues: Vec<FitIssue> = Vec::new();
294 let mandatory = self.mandatory_tags(type_name);
295 for tag in &mandatory {
296 if entity.missing(tag) {
297 issues.push(FitIssue::MissingMarker {
298 tag: tag.clone(),
299 spec: type_name.to_string(),
300 });
301 }
302 }
303 issues
304 }
305
306 pub fn validate_entity(&self, entity: &HDict) -> Vec<ValidationIssue> {
313 let mut issues: Vec<ValidationIssue> = Vec::new();
314 let ref_str = entity.id().map(|r| r.val.clone());
315
316 let tag_names: Vec<String> = entity.tag_names().map(|s| s.to_string()).collect();
319 for tag_name in &tag_names {
320 let val = entity.get(tag_name);
321 if !matches!(val, Some(Kind::Marker)) {
322 continue;
323 }
324 if !self.defs.contains_key(tag_name.as_str()) {
325 continue;
326 }
327 let mandatory = self.mandatory_tags(tag_name);
329 for m in &mandatory {
330 if entity.missing(m) {
331 issues.push(ValidationIssue {
332 entity: ref_str.clone(),
333 issue_type: "missing_marker".to_string(),
334 detail: format!(
335 "Entity claims '{}' but is missing mandatory marker '{}'",
336 tag_name, m
337 ),
338 });
339 }
340 }
341 }
342 issues
343 }
344
345 pub fn len(&self) -> usize {
349 self.defs.len()
350 }
351
352 pub fn is_empty(&self) -> bool {
354 self.defs.is_empty()
355 }
356
357 pub fn contains(&self, name: &str) -> bool {
359 self.defs.contains_key(name)
360 }
361
362 pub fn defs(&self) -> &HashMap<String, Def> {
364 &self.defs
365 }
366
367 pub fn libs(&self) -> &HashMap<String, Lib> {
369 &self.libs
370 }
371
372 pub fn taxonomy(&self) -> &TaxonomyTree {
374 &self.taxonomy
375 }
376
377 pub fn register_spec(&mut self, spec: Spec) {
381 let lib = spec.lib.clone();
382 let qname = spec.qname.clone();
383 self.specs.insert(qname.clone(), spec);
384 self.spec_libs.entry(lib).or_default().push(qname);
385 }
386
387 pub fn get_spec(&self, qname: &str) -> Option<&Spec> {
389 self.specs.get(qname)
390 }
391
392 pub fn specs(&self, lib: Option<&str>) -> Vec<&Spec> {
394 match lib {
395 Some(lib_name) => self
396 .spec_libs
397 .get(lib_name)
398 .map(|qnames| qnames.iter().filter_map(|q| self.specs.get(q)).collect())
399 .unwrap_or_default(),
400 None => self.specs.values().collect(),
401 }
402 }
403
404 pub fn specs_map(&self) -> &HashMap<String, Spec> {
406 &self.specs
407 }
408
409 pub fn set_lib_source(&mut self, lib_name: &str, source: LibSource) {
411 self.lib_sources.insert(lib_name.to_string(), source);
412 }
413
414 pub fn lib_source(&self, lib_name: &str) -> Option<&LibSource> {
416 self.lib_sources.get(lib_name)
417 }
418
419 pub fn export_lib_xeto(&self, lib_name: &str) -> Result<String, String> {
421 let lib = self
422 .libs()
423 .get(lib_name)
424 .ok_or_else(|| format!("library '{}' not found", lib_name))?;
425 let specs: Vec<&crate::xeto::Spec> = self.specs(Some(lib_name));
426 Ok(crate::xeto::export::export_lib(
427 lib_name,
428 &lib.version,
429 &lib.doc,
430 &lib.depends,
431 &specs,
432 ))
433 }
434
435 pub fn save_lib(&self, lib_name: &str, path: &std::path::Path) -> Result<(), String> {
437 let xeto_text = self.export_lib_xeto(lib_name)?;
438 std::fs::write(path, xeto_text).map_err(|e| format!("failed to write {:?}: {}", path, e))
439 }
440
441 pub fn load_xeto_str(
443 &mut self,
444 source: &str,
445 lib_name: &str,
446 ) -> Result<Vec<String>, crate::xeto::XetoError> {
447 let (lib, specs) = crate::xeto::loader::load_xeto_source(source, lib_name, self)?;
448 let qnames: Vec<String> = specs.iter().map(|s| s.qname.clone()).collect();
449 self.register_lib(lib);
450 for spec in specs {
451 self.register_spec(spec);
452 }
453 self.set_lib_source(lib_name, LibSource::Xeto(source.to_string()));
454 Ok(qnames)
455 }
456
457 pub fn load_xeto_dir(
459 &mut self,
460 dir: &std::path::Path,
461 ) -> Result<(String, Vec<String>), crate::xeto::XetoError> {
462 let (name, lib, specs) = crate::xeto::loader::load_xeto_dir(dir, self)?;
463 let qnames: Vec<String> = specs.iter().map(|s| s.qname.clone()).collect();
464 self.register_lib(lib);
465 for spec in specs {
466 self.register_spec(spec);
467 }
468 self.set_lib_source(&name, LibSource::Directory(dir.to_path_buf()));
469 Ok((name, qnames))
470 }
471
472 pub fn unload_lib(&mut self, lib_name: &str) -> Result<(), String> {
475 for (name, lib) in &self.libs {
477 if name != lib_name && lib.depends.contains(&lib_name.to_string()) {
478 return Err(format!(
479 "cannot unload '{}': library '{}' depends on it",
480 lib_name, name
481 ));
482 }
483 }
484 if matches!(self.lib_sources.get(lib_name), Some(LibSource::Bundled)) {
486 return Err(format!("cannot unload bundled library '{}'", lib_name));
487 }
488
489 if let Some(qnames) = self.spec_libs.remove(lib_name) {
491 for qname in &qnames {
492 self.specs.remove(qname);
493 }
494 }
495
496 self.defs.retain(|_, def| def.lib != lib_name);
498
499 self.libs.remove(lib_name);
501
502 self.lib_sources.remove(lib_name);
504
505 self.taxonomy.clear_cache();
507
508 Ok(())
509 }
510}
511
512impl Default for DefNamespace {
513 fn default() -> Self {
514 Self::new()
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use crate::kinds::HRef;
522
523 fn build_test_ns() -> DefNamespace {
525 let trio = "\
526def:^marker
527doc:\"Marker type\"
528is:[^marker]
529lib:^lib:ph
530---
531def:^entity
532doc:\"Top-level entity\"
533is:[^marker]
534lib:^lib:ph
535---
536def:^equip
537doc:\"Equipment\"
538is:[^entity]
539lib:^lib:phIoT
540mandatory
541---
542def:^point
543doc:\"Data point\"
544is:[^entity]
545lib:^lib:phIoT
546---
547def:^ahu
548doc:\"Air Handling Unit\"
549is:[^equip]
550lib:^lib:phIoT
551mandatory
552---
553def:^meter
554doc:\"Meter\"
555is:[^equip]
556lib:^lib:phIoT
557---
558def:^hot-water
559doc:\"Hot water\"
560is:[^marker]
561lib:^lib:phIoT
562---
563def:^site
564doc:\"A site\"
565is:[^entity]
566lib:^lib:ph
567---
568def:^ahuZoneDelivery
569doc:\"AHU zone delivery choice\"
570is:[^choice]
571lib:^lib:phIoT
572tagOn:[^ahu]
573---
574def:^directZone
575doc:\"Direct zone\"
576is:[^ahuZoneDelivery]
577lib:^lib:phIoT
578---
579def:^lib:ph
580doc:\"Project Haystack core\"
581is:[^lib]
582lib:^lib:ph
583version:\"4.0.0\"
584---
585def:^lib:phIoT
586doc:\"Project Haystack IoT\"
587is:[^lib]
588lib:^lib:phIoT
589version:\"4.0.0\"
590depends:[^lib:ph]
591";
592 let mut ns = DefNamespace::new();
593 let libs = load_trio(trio).unwrap();
594 for lib in libs {
595 ns.register_lib(lib);
596 }
597 ns
598 }
599
600 #[test]
601 fn new_namespace_is_empty() {
602 let ns = DefNamespace::new();
603 assert!(ns.is_empty());
604 assert_eq!(ns.len(), 0);
605 }
606
607 #[test]
608 fn register_and_get_def() {
609 let ns = build_test_ns();
610 assert!(ns.contains("ahu"));
611 assert!(ns.contains("equip"));
612 assert!(!ns.contains("nonexistent"));
613
614 let ahu = ns.get_def("ahu").unwrap();
615 assert_eq!(ahu.symbol, "ahu");
616 assert_eq!(ahu.is_, vec!["equip"]);
617 }
618
619 #[test]
620 fn is_a_direct_parent() {
621 let ns = build_test_ns();
622 assert!(ns.is_a("ahu", "equip"));
623 }
624
625 #[test]
626 fn is_a_ancestor() {
627 let ns = build_test_ns();
628 assert!(ns.is_a("ahu", "entity"));
629 assert!(ns.is_a("ahu", "marker"));
630 }
631
632 #[test]
633 fn is_a_self() {
634 let ns = build_test_ns();
635 assert!(ns.is_a("ahu", "ahu"));
636 }
637
638 #[test]
639 fn is_a_false_for_unrelated() {
640 let ns = build_test_ns();
641 assert!(!ns.is_a("ahu", "point"));
642 }
643
644 #[test]
645 fn subtypes_direct() {
646 let ns = build_test_ns();
647 let mut subs = ns.subtypes("equip");
648 subs.sort();
649 assert_eq!(subs, vec!["ahu", "meter"]);
650 }
651
652 #[test]
653 fn supertypes_chain() {
654 let ns = build_test_ns();
655 let supers = ns.supertypes("ahu");
656 assert_eq!(supers, vec!["equip", "entity", "marker"]);
658 }
659
660 #[test]
661 fn mandatory_tags_for_ahu() {
662 let ns = build_test_ns();
663 let tags = ns.mandatory_tags("ahu");
664 assert!(tags.contains("ahu"));
665 assert!(tags.contains("equip"));
666 assert!(!tags.contains("entity"));
668 }
669
670 #[test]
671 fn conjunct_parts_decompose() {
672 let ns = build_test_ns();
673 let parts = ns.conjunct_parts("hot-water").unwrap();
674 assert_eq!(parts, &["hot", "water"]);
675 }
676
677 #[test]
678 fn conjunct_parts_unknown() {
679 let ns = build_test_ns();
680 assert!(ns.conjunct_parts("site").is_none());
681 }
682
683 #[test]
684 fn fits_with_valid_entity() {
685 let ns = build_test_ns();
686 let mut entity = HDict::new();
687 entity.set("id", Kind::Ref(HRef::from_val("ahu-1")));
688 entity.set("ahu", Kind::Marker);
689 entity.set("equip", Kind::Marker);
690
691 assert!(ns.fits(&entity, "ahu"));
692 }
693
694 #[test]
695 fn fits_missing_mandatory() {
696 let ns = build_test_ns();
697 let mut entity = HDict::new();
698 entity.set("id", Kind::Ref(HRef::from_val("ahu-1")));
699 entity.set("ahu", Kind::Marker);
700 assert!(!ns.fits(&entity, "ahu"));
703 }
704
705 #[test]
706 fn fits_explain_missing_marker() {
707 let ns = build_test_ns();
708 let mut entity = HDict::new();
709 entity.set("id", Kind::Ref(HRef::from_val("ahu-1")));
710 entity.set("ahu", Kind::Marker);
711 let issues = ns.fits_explain(&entity, "ahu");
714 assert!(!issues.is_empty());
715
716 let has_equip_issue = issues.iter().any(|i| {
717 matches!(i, FitIssue::MissingMarker { tag, spec }
718 if tag == "equip" && spec == "ahu")
719 });
720 assert!(has_equip_issue);
721 }
722
723 #[test]
724 fn fits_explain_no_issues_when_valid() {
725 let ns = build_test_ns();
726 let mut entity = HDict::new();
727 entity.set("ahu", Kind::Marker);
728 entity.set("equip", Kind::Marker);
729
730 let issues = ns.fits_explain(&entity, "ahu");
731 assert!(issues.is_empty());
732 }
733
734 #[test]
735 fn validate_entity_finds_missing_markers() {
736 let ns = build_test_ns();
737 let mut entity = HDict::new();
738 entity.set("id", Kind::Ref(HRef::from_val("ahu-1")));
739 entity.set("ahu", Kind::Marker);
740 let issues = ns.validate_entity(&entity);
743 assert!(!issues.is_empty());
744
745 let has_issue = issues
746 .iter()
747 .any(|i| i.issue_type == "missing_marker" && i.detail.contains("equip"));
748 assert!(has_issue);
749 }
750
751 #[test]
752 fn validate_entity_no_issues_for_valid() {
753 let ns = build_test_ns();
754 let mut entity = HDict::new();
755 entity.set("id", Kind::Ref(HRef::from_val("ahu-1")));
756 entity.set("ahu", Kind::Marker);
757 entity.set("equip", Kind::Marker);
758
759 let issues = ns.validate_entity(&entity);
760 assert!(issues.is_empty());
761 }
762
763 #[test]
764 fn tags_for_entity_type() {
765 let ns = build_test_ns();
766 let tags = ns.tags_for("ahu");
767 assert!(tags.contains("ahuZoneDelivery"));
769 }
770
771 #[test]
772 fn choices_from_index() {
773 let ns = build_test_ns();
774 let options = ns.choices("ahuZoneDelivery");
775 assert!(options.contains(&"directZone".to_string()));
776 }
777
778 #[test]
779 fn libs_registered() {
780 let ns = build_test_ns();
781 assert!(ns.libs().contains_key("ph"));
782 assert!(ns.libs().contains_key("phIoT"));
783 }
784
785 #[test]
786 fn def_count() {
787 let ns = build_test_ns();
788 assert_eq!(ns.len(), 12);
791 }
792
793 #[test]
796 fn register_and_get_spec() {
797 let mut ns = DefNamespace::new();
798 let spec = crate::xeto::Spec::new("test::Foo", "test", "Foo");
799 ns.register_spec(spec);
800 assert!(ns.get_spec("test::Foo").is_some());
801 assert!(ns.get_spec("test::Bar").is_none());
802 }
803
804 #[test]
805 fn specs_filtered_by_lib() {
806 let mut ns = DefNamespace::new();
807 ns.register_spec(crate::xeto::Spec::new("test::Foo", "test", "Foo"));
808 ns.register_spec(crate::xeto::Spec::new("test::Bar", "test", "Bar"));
809 ns.register_spec(crate::xeto::Spec::new("other::Baz", "other", "Baz"));
810 assert_eq!(ns.specs(Some("test")).len(), 2);
811 assert_eq!(ns.specs(Some("other")).len(), 1);
812 assert_eq!(ns.specs(None).len(), 3);
813 }
814
815 #[test]
816 fn unload_lib_removes_specs() {
817 let mut ns = DefNamespace::new();
818 ns.register_spec(crate::xeto::Spec::new("test::Foo", "test", "Foo"));
819 ns.set_lib_source("test", LibSource::Xeto("...".into()));
820 ns.register_lib(crate::ontology::Lib {
821 name: "test".into(),
822 version: "1.0".into(),
823 doc: String::new(),
824 depends: vec![],
825 defs: std::collections::HashMap::new(),
826 });
827 assert!(ns.unload_lib("test").is_ok());
828 assert!(ns.get_spec("test::Foo").is_none());
829 assert!(ns.specs(Some("test")).is_empty());
830 }
831
832 #[test]
833 fn unload_bundled_fails() {
834 let mut ns = DefNamespace::new();
835 ns.set_lib_source("sys", LibSource::Bundled);
836 assert!(ns.unload_lib("sys").is_err());
837 }
838
839 #[test]
840 fn unload_with_dependent_fails() {
841 let mut ns = DefNamespace::new();
842 ns.register_lib(crate::ontology::Lib {
843 name: "base".into(),
844 version: "1.0".into(),
845 doc: String::new(),
846 depends: vec![],
847 defs: std::collections::HashMap::new(),
848 });
849 ns.register_lib(crate::ontology::Lib {
850 name: "child".into(),
851 version: "1.0".into(),
852 doc: String::new(),
853 depends: vec!["base".into()],
854 defs: std::collections::HashMap::new(),
855 });
856 ns.set_lib_source("base", LibSource::Xeto("...".into()));
857 assert!(ns.unload_lib("base").is_err());
858 }
859
860 #[test]
861 fn load_standard_includes_xeto_specs() {
862 let ns = DefNamespace::load_standard().unwrap();
863 let spec_count = ns.specs(None).len();
865 println!("loaded {} xeto specs from bundled libraries", spec_count);
866 assert!(spec_count > 0, "should have loaded some xeto specs");
867 }
868
869 #[test]
870 fn bundled_libs_cannot_be_unloaded() {
871 let mut ns = DefNamespace::load_standard().unwrap();
872 assert!(ns.unload_lib("ph").is_err());
875 }
876}