haystack_core/xeto/
loader.rs1use std::collections::{HashMap, HashSet};
4use std::path::Path;
5
6use crate::ontology::{DefNamespace, Lib};
7use crate::xeto::XetoError;
8use crate::xeto::parser::parse_xeto;
9use crate::xeto::resolver::XetoResolver;
10use crate::xeto::spec::{Spec, spec_from_def};
11
12pub fn load_xeto_source(
17 source: &str,
18 lib_name: &str,
19 ns: &DefNamespace,
20) -> Result<(Lib, Vec<Spec>), XetoError> {
21 let xeto_file = parse_xeto(source)?;
22 load_from_ast(xeto_file, lib_name, ns)
23}
24
25pub fn load_xeto_dir(dir: &Path, ns: &DefNamespace) -> Result<(String, Lib, Vec<Spec>), XetoError> {
30 let mut all_source = String::new();
31 let mut lib_name: Option<String> = None;
32
33 let mut entries: Vec<_> = std::fs::read_dir(dir)
35 .map_err(|e| XetoError::Load(format!("cannot read directory: {e}")))?
36 .filter_map(|e| e.ok())
37 .filter(|e| e.path().extension().is_some_and(|ext| ext == "xeto"))
38 .collect();
39 entries.sort_by_key(|e| e.file_name());
40
41 if entries.is_empty() {
42 return Err(XetoError::Load("no .xeto files found in directory".into()));
43 }
44
45 for entry in &entries {
46 let content = std::fs::read_to_string(entry.path())
47 .map_err(|e| XetoError::Load(format!("cannot read {:?}: {e}", entry.path())))?;
48
49 if lib_name.is_none()
51 && let Ok(xf) = parse_xeto(&content)
52 && let Some(ref pragma) = xf.pragma
53 {
54 lib_name = Some(pragma.name.clone());
55 }
56
57 all_source.push_str(&content);
58 all_source.push('\n');
59 }
60
61 let name = lib_name.unwrap_or_else(|| {
63 dir.file_name()
64 .and_then(|n| n.to_str())
65 .unwrap_or("unknown")
66 .to_string()
67 });
68
69 let (lib, specs) = load_xeto_source(&all_source, &name, ns)?;
70 Ok((name, lib, specs))
71}
72
73fn load_from_ast(
75 xeto_file: crate::xeto::ast::XetoFile,
76 lib_name: &str,
77 ns: &DefNamespace,
78) -> Result<(Lib, Vec<Spec>), XetoError> {
79 let mut resolver = XetoResolver::new();
81 for (name, lib) in ns.libs() {
82 let mut all_names: HashSet<String> = ns
83 .specs(Some(name))
84 .iter()
85 .map(|s| s.name.clone())
86 .collect();
87 for def_name in lib.defs.keys() {
89 all_names.insert(def_name.clone());
90 }
91 resolver.add_lib(name, all_names, lib.depends.clone());
92 }
93
94 let own_names: HashSet<String> = xeto_file.specs.iter().map(|s| s.name.clone()).collect();
96 let depends: Vec<String> = xeto_file
97 .pragma
98 .as_ref()
99 .map(|p| p.depends.clone())
100 .unwrap_or_default();
101 resolver.add_lib(lib_name, own_names, depends.clone());
102
103 for dep in &depends {
105 if !ns.libs().contains_key(dep.as_str()) {
106 return Err(XetoError::Load(format!(
107 "library '{}' depends on '{}' which is not loaded",
108 lib_name, dep
109 )));
110 }
111 }
112
113 let mut specs = Vec::new();
115 for spec_def in &xeto_file.specs {
116 let mut resolved = spec_from_def(spec_def, lib_name);
117
118 if let Some(ref base) = resolved.base
120 && let Some(resolved_name) = resolver.resolve(base, lib_name)
121 {
122 resolved.base = Some(resolved_name);
123 }
124
125 for slot in &mut resolved.slots {
127 if let Some(ref type_ref) = slot.type_ref
128 && let Some(resolved_name) = resolver.resolve(type_ref, lib_name)
129 {
130 slot.type_ref = Some(resolved_name);
131 }
132 }
133
134 specs.push(resolved);
135 }
136
137 let pragma = xeto_file.pragma.as_ref();
139 let lib = Lib {
140 name: lib_name.to_string(),
141 version: pragma
142 .map(|p| p.version.clone())
143 .unwrap_or_else(|| "0.0.0".into()),
144 doc: pragma.map(|p| p.doc.clone()).unwrap_or_default(),
145 depends,
146 defs: HashMap::new(), };
148
149 Ok((lib, specs))
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn empty_ns() -> DefNamespace {
157 DefNamespace::new()
158 }
159
160 #[test]
161 fn load_simple_spec() {
162 let source = r#"
163Foo: Obj {
164 name: Str
165 active
166}
167"#;
168 let ns = empty_ns();
169 let (lib, specs) = load_xeto_source(source, "test", &ns).unwrap();
170 assert_eq!(lib.name, "test");
171 assert_eq!(specs.len(), 1);
172 assert_eq!(specs[0].qname, "test::Foo");
173 assert_eq!(specs[0].slots.len(), 2);
174 }
175
176 #[test]
177 fn load_with_pragma() {
178 let source = r#"
179pragma: Lib <
180 doc: "Test library"
181 version: "1.0.0"
182>
183
184Bar: Obj {
185 count: Number
186}
187"#;
188 let ns = empty_ns();
189 let (lib, specs) = load_xeto_source(source, "testlib", &ns).unwrap();
190 assert_eq!(lib.version, "1.0.0");
191 assert_eq!(lib.doc, "Test library");
192 assert_eq!(specs.len(), 1);
193 }
194
195 #[test]
196 fn load_multiple_specs() {
197 let source = r#"
198Parent: Obj {
199 equip
200}
201
202Child: Parent {
203 ahu
204 dis: Str
205}
206"#;
207 let ns = empty_ns();
208 let (_, specs) = load_xeto_source(source, "test", &ns).unwrap();
209 assert_eq!(specs.len(), 2);
210 let child = specs.iter().find(|s| s.name == "Child").unwrap();
211 assert_eq!(child.base.as_deref(), Some("test::Parent"));
212 }
213
214 #[test]
215 fn load_registers_in_namespace() {
216 let source = "Baz: Obj { tag }";
217 let mut ns = DefNamespace::new();
218 let qnames = ns.load_xeto_str(source, "mylib").unwrap();
219 assert_eq!(qnames, vec!["mylib::Baz"]);
220 assert!(ns.get_spec("mylib::Baz").is_some());
221 }
222
223 #[test]
224 fn load_missing_dependency_fails() {
225 let source = r#"
226pragma: Lib <
227 doc: "Needs base"
228 version: "1.0.0"
229 depends: { { lib: "nonexistent" } }
230>
231
232Foo: Obj { tag }
233"#;
234 let ns = empty_ns();
235 let result = load_xeto_source(source, "test", &ns);
236 assert!(result.is_err());
237 }
238
239 #[test]
240 fn load_and_unload_roundtrip() {
241 let source = "Foo: Obj { marker }";
242 let mut ns = DefNamespace::new();
243 ns.load_xeto_str(source, "temp").unwrap();
244 assert!(ns.get_spec("temp::Foo").is_some());
245 ns.unload_lib("temp").unwrap();
246 assert!(ns.get_spec("temp::Foo").is_none());
247 }
248}