Skip to main content

haystack_core/xeto/
loader.rs

1//! Xeto library loader — parses .xeto source, resolves names, produces Specs.
2
3use 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
12/// Load a Xeto library from source text.
13///
14/// Parses the source, resolves names against already-loaded libraries in `ns`,
15/// and returns the library metadata plus resolved specs.
16pub 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
25/// Load a Xeto library from a directory containing .xeto files.
26///
27/// Reads all .xeto files in the directory, concatenates them (pragma from
28/// the first file that has one), and processes as a single library.
29pub 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    // Read .xeto files sorted by name for deterministic ordering
34    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        // Try to extract lib name from pragma if we haven't found one yet
50        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    // Fall back to directory name if no pragma found
62    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
73/// Internal: convert a parsed XetoFile into a Lib + Vec<Spec>.
74fn load_from_ast(
75    xeto_file: crate::xeto::ast::XetoFile,
76    lib_name: &str,
77    ns: &DefNamespace,
78) -> Result<(Lib, Vec<Spec>), XetoError> {
79    // Build resolver with known libs
80    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        // Also include def names for resolution
88        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    // Register this library's spec names for self-resolution
95    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    // Validate dependencies exist
104    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    // Resolve names and convert to Specs
114    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        // Resolve base type name
119        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        // Resolve slot type_refs
126        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    // Build Lib metadata
138    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(), // Specs are registered separately
147    };
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}