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}