Skip to main content

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.iter().any(|i| !matches!(i, ImportSource::Path { .. }));
175        if has_remote_imports && !skip_remote {
176            let needed_packages = Self::extract_referenced_packages(&contents, &package_name);
177            if !needed_packages.is_empty() {
178                let unresolved: Vec<String> = needed_packages
179                    .iter()
180                    .filter(|p| !self.package_types.contains_key(*p))
181                    .cloned()
182                    .collect();
183
184                if !unresolved.is_empty() {
185                    if verbose {
186                        println!(
187                            "    [~] Discovering siblings for unresolved packages: {:?}",
188                            unresolved
189                        );
190                    }
191
192                    /* Build a map of package → file path from sibling directories */
193                    let mut scan_dirs: Vec<PathBuf> = Vec::new();
194                    if let Some(parent) = file_path.parent() {
195                        scan_dirs.push(parent.to_path_buf());
196                    }
197                    scan_dirs.extend(self.include_dirs.iter().cloned());
198
199                    for dir in &scan_dirs {
200                        if let Ok(entries) = std::fs::read_dir(dir) {
201                            let mut paths: Vec<_> = entries
202                                .flatten()
203                                .map(|e| e.path())
204                                .collect();
205                            paths.sort();
206                            for path in paths {
207                                if path.extension().and_then(|e| e.to_str()) != Some("yaml") {
208                                    continue;
209                                }
210                                if path.file_name().and_then(|n| n.to_str())
211                                    .map_or(true, |n| !n.ends_with(".abi.yaml"))
212                                {
213                                    continue;
214                                }
215
216                                if let Ok(cp) = path.canonicalize() {
217                                    if self.loaded_files.contains(&cp) {
218                                        continue;
219                                    }
220                                }
221
222                                /* Peek at package without full parse */
223                                let sibling_contents = match std::fs::read_to_string(&path) {
224                                    Ok(c) => c,
225                                    Err(_) => continue,
226                                };
227                                let sibling_package =
228                                    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,
253                                    verbose,
254                                    true, /* skip_remote to prevent cascading */
255                                ) {
256                                    if verbose {
257                                        println!(
258                                            "    [~] Skipping sibling {}: {}",
259                                            path.display(),
260                                            e
261                                        );
262                                    }
263                                }
264                            }
265                        }
266                    }
267                }
268            }
269        }
270
271        /* Push this file last so that the root file (the one the caller
272           originally requested) ends up at the tail of all_files. The
273           flatten code relies on all_files.last() being the root. */
274        self.all_files.push(abi_file);
275
276        Ok(())
277    }
278
279    /* Get all collected type definitions */
280    pub fn get_all_types(&self) -> &[TypeDef] {
281        &self.all_types
282    }
283
284    /* Get all loaded ABI files */
285    pub fn get_all_files(&self) -> &[AbiFile] {
286        &self.all_files
287    }
288
289    /* Get the number of loaded files */
290    pub fn loaded_file_count(&self) -> usize {
291        self.loaded_files.len()
292    }
293
294    /* Resolve a type name which may be FQDN or simple name */
295    pub fn resolve_type_name(&self, type_name: &str) -> Option<String> {
296        /* If it contains a dot, it's potentially an FQDN */
297        if type_name.contains('.') {
298            /* Try to find the type by FQDN */
299            /* Format: package.name.TypeName or just TypeName */
300            let parts: Vec<&str> = type_name.split('.').collect();
301            if parts.len() < 2 {
302                /* Not a valid FQDN, return as-is */
303                return Some(type_name.to_string());
304            }
305
306            /* The last part is the type name */
307            let simple_name = parts[parts.len() - 1];
308
309            /* Try to match package prefixes */
310            for (package, types) in &self.package_types {
311                if type_name.starts_with(package) && types.contains(&simple_name.to_string()) {
312                    return Some(simple_name.to_string());
313                }
314            }
315
316            /* Not found by FQDN, maybe it's just a simple name with dots */
317            Some(type_name.to_string())
318        } else {
319            /* Simple name, return as-is */
320            Some(type_name.to_string())
321        }
322    }
323
324    /* Get the package name for a given type */
325    pub fn get_package_for_type(&self, type_name: &str) -> Option<String> {
326        for (package, types) in &self.package_types {
327            if types.contains(&type_name.to_string()) {
328                return Some(package.clone());
329            }
330        }
331        None
332    }
333
334    /* Get all packages */
335    pub fn get_packages(&self) -> Vec<String> {
336        self.package_types.keys().cloned().collect()
337    }
338
339    /* Extract packages referenced by type-refs in the raw YAML content.
340       Scans for `package:` lines (used in type-ref definitions) and returns
341       unique package names excluding the file's own package. */
342    fn extract_referenced_packages(contents: &str, own_package: &str) -> Vec<String> {
343        let mut packages = HashSet::new();
344        for line in contents.lines() {
345            let trimmed = line.trim();
346            if let Some(rest) = trimmed.strip_prefix("package:") {
347                let value = rest.trim().trim_matches('"').trim_matches('\'');
348                if !value.is_empty() && value != own_package {
349                    packages.insert(value.to_string());
350                }
351            }
352        }
353        packages.into_iter().collect()
354    }
355
356    /* Extract the top-level package name from raw YAML content without
357       doing a full parse. Looks for the `package:` field in the abi header. */
358    fn extract_own_package(contents: &str) -> Option<String> {
359        for line in contents.lines() {
360            let trimmed = line.trim();
361            if let Some(rest) = trimmed.strip_prefix("package:") {
362                let value = rest.trim().trim_matches('"').trim_matches('\'');
363                if !value.is_empty() {
364                    return Some(value.to_string());
365                }
366            }
367        }
368        None
369    }
370}