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
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}