haystack_core/xeto/
resolver.rs1use std::collections::{HashMap, HashSet};
4
5use super::XetoError;
6
7pub struct XetoResolver {
16 lib_specs: HashMap<String, HashSet<String>>,
18 lib_depends: HashMap<String, Vec<String>>,
20}
21
22impl XetoResolver {
23 pub fn new() -> Self {
25 Self {
26 lib_specs: HashMap::new(),
27 lib_depends: HashMap::new(),
28 }
29 }
30
31 pub fn add_lib(&mut self, lib_name: &str, spec_names: HashSet<String>, depends: Vec<String>) {
33 self.lib_specs.insert(lib_name.to_string(), spec_names);
34 self.lib_depends.insert(lib_name.to_string(), depends);
35 }
36
37 pub fn resolve(&self, name: &str, context_lib: &str) -> Option<String> {
41 if name.contains("::") {
43 return Some(name.to_string());
44 }
45
46 if let Some(specs) = self.lib_specs.get(context_lib)
48 && specs.contains(name)
49 {
50 return Some(format!("{}::{}", context_lib, name));
51 }
52
53 if let Some(deps) = self.lib_depends.get(context_lib) {
55 for dep in deps {
56 if let Some(specs) = self.lib_specs.get(dep.as_str())
57 && specs.contains(name)
58 {
59 return Some(format!("{}::{}", dep, name));
60 }
61 }
62 }
63
64 if let Some(specs) = self.lib_specs.get("sys")
66 && specs.contains(name)
67 {
68 return Some(format!("sys::{}", name));
69 }
70
71 for (lib_name, specs) in &self.lib_specs {
73 if lib_name == context_lib || lib_name == "sys" {
74 continue;
75 }
76 if specs.contains(name) {
77 return Some(format!("{}::{}", lib_name, name));
78 }
79 }
80
81 None
82 }
83
84 pub fn dependency_order(&self) -> Result<Vec<String>, XetoError> {
88 let mut result = Vec::new();
89 let mut visited = HashSet::new();
90 let mut in_progress = HashSet::new();
91
92 let all_libs: Vec<String> = self.lib_specs.keys().cloned().collect();
94 for lib in &all_libs {
95 if !visited.contains(lib.as_str()) {
96 self.topo_visit(lib, &mut visited, &mut in_progress, &mut result)?;
97 }
98 }
99
100 Ok(result)
101 }
102
103 fn topo_visit(
105 &self,
106 lib: &str,
107 visited: &mut HashSet<String>,
108 in_progress: &mut HashSet<String>,
109 result: &mut Vec<String>,
110 ) -> Result<(), XetoError> {
111 if in_progress.contains(lib) {
112 return Err(XetoError::Resolve(format!(
113 "circular dependency detected involving '{}'",
114 lib
115 )));
116 }
117 if visited.contains(lib) {
118 return Ok(());
119 }
120
121 in_progress.insert(lib.to_string());
122
123 if let Some(deps) = self.lib_depends.get(lib) {
125 for dep in deps {
126 self.topo_visit(dep, visited, in_progress, result)?;
127 }
128 }
129
130 in_progress.remove(lib);
131 visited.insert(lib.to_string());
132 result.push(lib.to_string());
133
134 Ok(())
135 }
136}
137
138impl Default for XetoResolver {
139 fn default() -> Self {
140 Self::new()
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn make_resolver() -> XetoResolver {
149 let mut resolver = XetoResolver::new();
150
151 let mut sys_specs = HashSet::new();
152 sys_specs.insert("Str".to_string());
153 sys_specs.insert("Number".to_string());
154 sys_specs.insert("Ref".to_string());
155 sys_specs.insert("Bool".to_string());
156 resolver.add_lib("sys", sys_specs, vec![]);
157
158 let mut ph_specs = HashSet::new();
159 ph_specs.insert("Entity".to_string());
160 ph_specs.insert("Site".to_string());
161 resolver.add_lib("ph", ph_specs, vec!["sys".to_string()]);
162
163 let mut phiot_specs = HashSet::new();
164 phiot_specs.insert("Equip".to_string());
165 phiot_specs.insert("Point".to_string());
166 phiot_specs.insert("Ahu".to_string());
167 resolver.add_lib(
168 "phIoT",
169 phiot_specs,
170 vec!["ph".to_string(), "sys".to_string()],
171 );
172
173 resolver
174 }
175
176 #[test]
177 fn resolve_qualified_returns_as_is() {
178 let resolver = make_resolver();
179 assert_eq!(
180 resolver.resolve("ph::Entity", "phIoT"),
181 Some("ph::Entity".to_string())
182 );
183 }
184
185 #[test]
186 fn resolve_finds_in_current_lib() {
187 let resolver = make_resolver();
188 assert_eq!(
189 resolver.resolve("Ahu", "phIoT"),
190 Some("phIoT::Ahu".to_string())
191 );
192 }
193
194 #[test]
195 fn resolve_finds_in_dependencies() {
196 let resolver = make_resolver();
197 assert_eq!(
198 resolver.resolve("Entity", "phIoT"),
199 Some("ph::Entity".to_string())
200 );
201 }
202
203 #[test]
204 fn resolve_finds_in_sys() {
205 let resolver = make_resolver();
206 assert_eq!(
207 resolver.resolve("Str", "phIoT"),
208 Some("sys::Str".to_string())
209 );
210 }
211
212 #[test]
213 fn resolve_unknown_returns_none() {
214 let resolver = make_resolver();
215 assert_eq!(resolver.resolve("UnknownType", "phIoT"), None);
216 }
217
218 #[test]
219 fn resolve_fallback_to_all_libs() {
220 let mut resolver = XetoResolver::new();
224
225 let mut ph_specs = HashSet::new();
226 ph_specs.insert("Entity".to_string());
227 resolver.add_lib("ph", ph_specs, vec![]);
228
229 let mut other_specs = HashSet::new();
230 other_specs.insert("Foo".to_string());
231 resolver.add_lib("other", other_specs, vec![]); let result = resolver.resolve("Entity", "other");
235 assert_eq!(result, Some("ph::Entity".to_string()));
236 }
237
238 #[test]
239 fn dependency_ordering() {
240 let resolver = make_resolver();
241 let order = resolver.dependency_order().unwrap();
242
243 let sys_pos = order.iter().position(|s| s == "sys").unwrap();
245 let ph_pos = order.iter().position(|s| s == "ph").unwrap();
246 let phiot_pos = order.iter().position(|s| s == "phIoT").unwrap();
247
248 assert!(sys_pos < ph_pos);
249 assert!(ph_pos < phiot_pos);
250 }
251
252 #[test]
253 fn circular_dependency_detected() {
254 let mut resolver = XetoResolver::new();
255
256 let a_specs = HashSet::new();
257 resolver.add_lib("a", a_specs, vec!["b".to_string()]);
258
259 let b_specs = HashSet::new();
260 resolver.add_lib("b", b_specs, vec!["a".to_string()]);
261
262 let result = resolver.dependency_order();
263 assert!(result.is_err());
264 let err = result.unwrap_err();
265 assert!(err.to_string().contains("circular dependency"));
266 }
267
268 #[test]
269 fn dependency_order_single_lib() {
270 let mut resolver = XetoResolver::new();
271 let specs = HashSet::new();
272 resolver.add_lib("solo", specs, vec![]);
273
274 let order = resolver.dependency_order().unwrap();
275 assert_eq!(order, vec!["solo"]);
276 }
277
278 #[test]
279 fn resolve_sys_builtin_from_any_context() {
280 let resolver = make_resolver();
281 assert_eq!(
282 resolver.resolve("Number", "ph"),
283 Some("sys::Number".to_string())
284 );
285 }
286}