abi_loader/resolver.rs
1use abi_types::TypeDef;
2use std::collections::HashSet;
3use std::path::{Path, PathBuf};
4
5use crate::file::{AbiFile, ImportSource};
6
7/* Import resolver for loading and merging imported ABI files */
8pub struct ImportResolver {
9 /* Track loaded files to detect circular imports */
10 loaded_files: HashSet<PathBuf>,
11
12 /* Include directories for searching imports */
13 include_dirs: Vec<PathBuf>,
14
15 /* All collected type definitions */
16 all_types: Vec<TypeDef>,
17
18 /* All loaded ABI files */
19 all_files: Vec<AbiFile>,
20
21 /* Map from package name to list of types in that package */
22 package_types: std::collections::HashMap<String, Vec<String>>,
23}
24
25impl ImportResolver {
26 /* Create a new import resolver with the given include directories */
27 pub fn new(include_dirs: Vec<PathBuf>) -> Self {
28 Self {
29 loaded_files: HashSet::new(),
30 include_dirs,
31 all_types: Vec::new(),
32 all_files: Vec::new(),
33 package_types: std::collections::HashMap::new(),
34 }
35 }
36
37 /* Resolve an import path relative to a base file or include directories */
38 fn resolve_import_path(&self, import_path: &str, base_file: &Path) -> anyhow::Result<PathBuf> {
39 /* First try relative to the base file's directory */
40 if let Some(parent) = base_file.parent() {
41 let relative_path = parent.join(import_path);
42 if relative_path.exists() {
43 return Ok(relative_path.canonicalize()?);
44 }
45 }
46
47 /* Then try each include directory */
48 for include_dir in &self.include_dirs {
49 let include_path = include_dir.join(import_path);
50 if include_path.exists() {
51 return Ok(include_path.canonicalize()?);
52 }
53 }
54
55 anyhow::bail!(
56 "Import '{}' not found relative to '{}' or in include directories",
57 import_path,
58 base_file.display()
59 )
60 }
61
62 /* Load an ABI file and recursively load its imports */
63 pub fn load_file_with_imports(
64 &mut self,
65 file_path: &Path,
66 verbose: bool,
67 ) -> anyhow::Result<()> {
68 self.load_file_with_imports_internal(file_path, verbose, false)
69 }
70
71 /* Load an ABI file and recursively load only local (path) imports */
72 pub fn load_file_with_imports_skip_remote(
73 &mut self,
74 file_path: &Path,
75 verbose: bool,
76 ) -> anyhow::Result<()> {
77 self.load_file_with_imports_internal(file_path, verbose, true)
78 }
79
80 fn load_file_with_imports_internal(
81 &mut self,
82 file_path: &Path,
83 verbose: bool,
84 skip_remote: bool,
85 ) -> anyhow::Result<()> {
86 /* Canonicalize the path to detect duplicates */
87 let canonical_path = file_path.canonicalize()?;
88
89 /* Skip if already loaded */
90 if self.loaded_files.contains(&canonical_path) {
91 if verbose {
92 println!(
93 " [~] Skipping already loaded file: {}",
94 file_path.display()
95 );
96 }
97 return Ok(());
98 }
99
100 /* Mark as loaded before processing imports to detect circular dependencies */
101 self.loaded_files.insert(canonical_path.clone());
102
103 if verbose {
104 println!("[~] Loading ABI file: {}", file_path.display());
105 }
106
107 /* Read and parse the ABI file */
108 let file = std::fs::File::open(file_path)?;
109 let contents = std::io::read_to_string(file)?;
110 let abi_file: AbiFile = serde_yml::from_str(&contents)?;
111
112 if verbose {
113 println!(" Package: {}", abi_file.package());
114 println!(" Version: {}", abi_file.package_version());
115 if !abi_file.imports().is_empty() {
116 println!(" Imports: {}", abi_file.imports().len());
117 }
118 }
119
120 /* Reserve the package name before processing imports so that sibling
121 auto-discovery can detect packages already being loaded and skip
122 duplicate files (e.g. flat variants of the same ABI). */
123 let package_name = abi_file.package().to_string();
124 self.package_types
125 .entry(package_name.clone())
126 .or_insert_with(Vec::new);
127
128 /* Recursively load imports (only path imports supported in this resolver) */
129 let imports = abi_file.imports().to_vec();
130 for import in &imports {
131 match import {
132 ImportSource::Path { path } => {
133 if verbose {
134 println!(" [~] Resolving path import: {}", path);
135 }
136
137 let import_path = self.resolve_import_path(path, file_path)?;
138
139 /* Recursively load the imported file */
140 self.load_file_with_imports_internal(&import_path, verbose, skip_remote)?;
141 }
142 _ => {
143 if verbose {
144 println!(
145 " [~] Remote import encountered, will resolve via sibling discovery: {:?}",
146 import
147 );
148 }
149 /* Remote imports are resolved after all imports are processed
150 by discovering sibling ABI files that provide needed packages. */
151 }
152 }
153 }
154
155 /* Add types from this file and register them with the package */
156 let type_names: Vec<String> = abi_file
157 .get_types()
158 .iter()
159 .map(|t| t.name.clone())
160 .collect();
161
162 self.all_types.extend(abi_file.get_types().to_vec());
163
164 /* Register types with their package */
165 self.package_types
166 .entry(package_name.clone())
167 .or_insert_with(Vec::new)
168 .extend(type_names);
169
170 /* If the file had remote imports and we are not in skip_remote mode,
171 discover sibling ABI files that provide the packages referenced by
172 this file's type-refs. Only run for top-level loads, not for
173 auto-discovered siblings (which use skip_remote=true). */
174 let has_remote_imports = imports
175 .iter()
176 .any(|i| !matches!(i, ImportSource::Path { .. }));
177 if has_remote_imports && !skip_remote {
178 let needed_packages = Self::extract_referenced_packages(&contents, &package_name);
179 if !needed_packages.is_empty() {
180 let unresolved: Vec<String> = needed_packages
181 .iter()
182 .filter(|p| !self.package_types.contains_key(*p))
183 .cloned()
184 .collect();
185
186 if !unresolved.is_empty() {
187 if verbose {
188 println!(
189 " [~] Discovering siblings for unresolved packages: {:?}",
190 unresolved
191 );
192 }
193
194 /* Build a map of package → file path from sibling directories */
195 let mut scan_dirs: Vec<PathBuf> = Vec::new();
196 if let Some(parent) = file_path.parent() {
197 scan_dirs.push(parent.to_path_buf());
198 }
199 scan_dirs.extend(self.include_dirs.iter().cloned());
200
201 for dir in &scan_dirs {
202 if let Ok(entries) = std::fs::read_dir(dir) {
203 let mut paths: Vec<_> = entries.flatten().map(|e| e.path()).collect();
204 paths.sort();
205 for path in paths {
206 if path.extension().and_then(|e| e.to_str()) != Some("yaml") {
207 continue;
208 }
209 if path
210 .file_name()
211 .and_then(|n| n.to_str())
212 .map_or(true, |n| !n.ends_with(".abi.yaml"))
213 {
214 continue;
215 }
216
217 if let Ok(cp) = path.canonicalize() {
218 if self.loaded_files.contains(&cp) {
219 continue;
220 }
221 }
222
223 /* Peek at package without full parse */
224 let sibling_contents = match std::fs::read_to_string(&path) {
225 Ok(c) => c,
226 Err(_) => continue,
227 };
228 let sibling_package = Self::extract_own_package(&sibling_contents);
229 let sibling_package = match sibling_package {
230 Some(p) => p,
231 None => continue,
232 };
233
234 /* Check against both the original unresolved set and the
235 live package_types (a sibling loaded earlier in this scan
236 may have already provided this package). */
237 if !unresolved.contains(&sibling_package)
238 || self.package_types.contains_key(&sibling_package)
239 {
240 continue;
241 }
242
243 if verbose {
244 println!(
245 " [~] Auto-loading sibling {} for package '{}'",
246 path.display(),
247 sibling_package
248 );
249 }
250
251 if let Err(e) = self.load_file_with_imports_internal(
252 &path, verbose,
253 true, /* skip_remote to prevent cascading */
254 ) {
255 if verbose {
256 println!(
257 " [~] Skipping sibling {}: {}",
258 path.display(),
259 e
260 );
261 }
262 }
263 }
264 }
265 }
266 }
267 }
268 }
269
270 /* Push this file last so that the root file (the one the caller
271 originally requested) ends up at the tail of all_files. The
272 flatten code relies on all_files.last() being the root. */
273 self.all_files.push(abi_file);
274
275 Ok(())
276 }
277
278 /* Get all collected type definitions */
279 pub fn get_all_types(&self) -> &[TypeDef] {
280 &self.all_types
281 }
282
283 /* Get all loaded ABI files */
284 pub fn get_all_files(&self) -> &[AbiFile] {
285 &self.all_files
286 }
287
288 /* Get the number of loaded files */
289 pub fn loaded_file_count(&self) -> usize {
290 self.loaded_files.len()
291 }
292
293 /* Resolve a type name which may be FQDN or simple name */
294 pub fn resolve_type_name(&self, type_name: &str) -> Option<String> {
295 /* If it contains a dot, it's potentially an FQDN */
296 if type_name.contains('.') {
297 /* Try to find the type by FQDN */
298 /* Format: package.name.TypeName or just TypeName */
299 let parts: Vec<&str> = type_name.split('.').collect();
300 if parts.len() < 2 {
301 /* Not a valid FQDN, return as-is */
302 return Some(type_name.to_string());
303 }
304
305 /* The last part is the type name */
306 let simple_name = parts[parts.len() - 1];
307
308 /* Try to match package prefixes */
309 for (package, types) in &self.package_types {
310 if type_name.starts_with(package) && types.contains(&simple_name.to_string()) {
311 return Some(simple_name.to_string());
312 }
313 }
314
315 /* Not found by FQDN, maybe it's just a simple name with dots */
316 Some(type_name.to_string())
317 } else {
318 /* Simple name, return as-is */
319 Some(type_name.to_string())
320 }
321 }
322
323 /* Get the package name for a given type */
324 pub fn get_package_for_type(&self, type_name: &str) -> Option<String> {
325 for (package, types) in &self.package_types {
326 if types.contains(&type_name.to_string()) {
327 return Some(package.clone());
328 }
329 }
330 None
331 }
332
333 /* Get all packages */
334 pub fn get_packages(&self) -> Vec<String> {
335 self.package_types.keys().cloned().collect()
336 }
337
338 /* Extract packages referenced by type-refs in the raw YAML content.
339 Scans for `package:` lines (used in type-ref definitions) and returns
340 unique package names excluding the file's own package. */
341 fn extract_referenced_packages(contents: &str, own_package: &str) -> Vec<String> {
342 let mut packages = HashSet::new();
343 for line in contents.lines() {
344 let trimmed = line.trim();
345 if let Some(rest) = trimmed.strip_prefix("package:") {
346 let value = rest.trim().trim_matches('"').trim_matches('\'');
347 if !value.is_empty() && value != own_package {
348 packages.insert(value.to_string());
349 }
350 }
351 }
352 packages.into_iter().collect()
353 }
354
355 /* Extract the top-level package name from raw YAML content without
356 doing a full parse. Looks for the `package:` field in the abi header. */
357 fn extract_own_package(contents: &str) -> Option<String> {
358 for line in contents.lines() {
359 let trimmed = line.trim();
360 if let Some(rest) = trimmed.strip_prefix("package:") {
361 let value = rest.trim().trim_matches('"').trim_matches('\'');
362 if !value.is_empty() {
363 return Some(value.to_string());
364 }
365 }
366 }
367 None
368 }
369}