Skip to main content

abi_loader/
enhanced_resolver.rs

1//! Enhanced Import Resolver
2//!
3//! This module provides the full import resolution system that supports all import
4//! types (path, git, http, onchain) with cycle detection, version conflict detection,
5//! and the local import restriction rule.
6
7use std::collections::{HashMap, HashSet};
8use std::path::PathBuf;
9
10use crate::fetcher::{CompositeFetcher, FetchContext, FetchError, FetcherConfig};
11use crate::file::{AbiFile, ImportSource};
12use crate::package::{PackageId, ResolutionResult, ResolveError, ResolvedPackage};
13
14/* ============================================================================
15   Enhanced Import Resolver
16   ============================================================================ */
17
18/* Full-featured import resolver supporting all import types */
19pub struct EnhancedImportResolver {
20    /* Composite fetcher for handling all import types */
21    fetcher: CompositeFetcher,
22
23    /* Include directories for path resolution */
24    include_dirs: Vec<PathBuf>,
25
26    /* Enable verbose logging */
27    verbose: bool,
28}
29
30impl EnhancedImportResolver {
31    /* Create a new enhanced import resolver with the given configuration */
32    pub fn new(config: FetcherConfig, include_dirs: Vec<PathBuf>) -> Result<Self, ResolveError> {
33        let fetcher = CompositeFetcher::new(config)
34            .map_err(|e| ResolveError::InitError { message: e.to_string() })?;
35        Ok(Self {
36            fetcher,
37            include_dirs,
38            verbose: false,
39        })
40    }
41
42    /* Create with default configuration (all import types enabled) */
43    pub fn with_defaults(include_dirs: Vec<PathBuf>) -> Result<Self, ResolveError> {
44        Self::new(FetcherConfig::cli_default(), include_dirs)
45    }
46
47    /* Enable verbose logging */
48    pub fn with_verbose(mut self, verbose: bool) -> Self {
49        self.verbose = verbose;
50        self
51    }
52
53    /* Get the fetcher configuration */
54    pub fn config(&self) -> &FetcherConfig {
55        self.fetcher.config()
56    }
57
58    /* Resolve a root ABI file and all its transitive imports */
59    pub fn resolve_file(&self, file_path: &PathBuf) -> Result<ResolutionResult, ResolveError> {
60        /* Create root import source */
61        let root_source = ImportSource::Path {
62            path: file_path.to_string_lossy().to_string(),
63        };
64
65        /* Create root context */
66        let root_ctx = FetchContext::for_root(Some(file_path.clone()), self.include_dirs.clone());
67
68        /* Initialize resolution state */
69        let mut state = ResolutionState::new();
70
71        /* Resolve recursively */
72        let root_id = self.resolve_import(&root_source, &root_ctx, &mut state)?;
73
74        /* Build result */
75        let root_package = state
76            .resolved_packages
77            .get(&root_id)
78            .cloned()
79            .ok_or_else(|| ResolveError::FetchError {
80                source: root_source,
81                message: "Root package not found in resolution state".to_string(),
82            })?;
83
84        Ok(ResolutionResult {
85            root: root_package,
86            all_packages: state.resolved_packages.into_values().collect(),
87        })
88    }
89
90    /* Resolve an ABI from raw YAML content (for WASM/embedded use) */
91    pub fn resolve_content(
92        &self,
93        content: &str,
94        canonical_location: &str,
95    ) -> Result<ResolutionResult, ResolveError> {
96        /* Parse the ABI file */
97        let abi_file: AbiFile = serde_yml::from_str(content).map_err(|e| ResolveError::ParseError {
98            location: canonical_location.to_string(),
99            message: e.to_string(),
100        })?;
101
102        /* Create a synthetic import source */
103        let root_source = ImportSource::Path {
104            path: canonical_location.to_string(),
105        };
106
107        /* Initialize resolution state */
108        let mut state = ResolutionState::new();
109
110        /* Create root context - not remote since content is provided directly */
111        let root_ctx = FetchContext::for_root(None, self.include_dirs.clone());
112
113        /* Process this package directly */
114        let pkg_id = PackageId::from_abi_file(&abi_file);
115
116        /* Check for version conflict */
117        self.check_version_conflict(&pkg_id, &state)?;
118
119        /* Mark as being resolved (for cycle detection) */
120        state.in_progress.insert(canonical_location.to_string());
121        state.resolution_chain.push(pkg_id.clone());
122
123        /* Resolve all imports */
124        let mut dependencies = Vec::new();
125        for import in abi_file.imports() {
126            let child_ctx = root_ctx.child_context(import, None);
127            let dep_id = self.resolve_import(import, &child_ctx, &mut state)?;
128            dependencies.push(dep_id);
129        }
130
131        /* Create resolved package */
132        let resolved = ResolvedPackage::new(root_source.clone(), abi_file, dependencies);
133
134        /* Mark as fully resolved */
135        state.in_progress.remove(canonical_location);
136        state.resolution_chain.pop();
137        state.resolved_packages.insert(pkg_id.clone(), resolved.clone());
138        state.versions.insert(pkg_id.package_name.clone(), pkg_id.version.clone());
139
140        Ok(ResolutionResult {
141            root: resolved,
142            all_packages: state.resolved_packages.into_values().collect(),
143        })
144    }
145
146    /* Internal: Resolve a single import and its transitive dependencies */
147    fn resolve_import(
148        &self,
149        source: &ImportSource,
150        ctx: &FetchContext,
151        state: &mut ResolutionState,
152    ) -> Result<PackageId, ResolveError> {
153        /* Fetch the content */
154        let fetch_result = self.fetcher.fetch(source, ctx).map_err(|e| match e {
155            FetchError::NotAllowed(s) => ResolveError::ImportTypeNotAllowed {
156                source: s,
157                reason: "Import type not allowed by configuration".to_string(),
158            },
159            FetchError::LocalFromRemote(path) => ResolveError::LocalImportFromRemote {
160                remote_package: state
161                    .resolution_chain
162                    .last()
163                    .cloned()
164                    .unwrap_or_else(|| PackageId::new("<root>", "0.0.0")),
165                local_import: ImportSource::Path { path },
166            },
167            FetchError::RevisionMismatch { required, actual } => ResolveError::RevisionMismatch {
168                source: source.clone(),
169                required,
170                actual,
171            },
172            _ => ResolveError::FetchError {
173                source: source.clone(),
174                message: e.to_string(),
175            },
176        })?;
177
178        if self.verbose {
179            println!("[~] Fetched: {}", fetch_result.canonical_location);
180        }
181
182        /* Check for cycle using canonical location */
183        if state.in_progress.contains(&fetch_result.canonical_location) {
184            return Err(ResolveError::CyclicDependency {
185                package_id: state
186                    .resolution_chain
187                    .last()
188                    .cloned()
189                    .unwrap_or_else(|| PackageId::new("<unknown>", "0.0.0")),
190                cycle_chain: state.resolution_chain.clone(),
191            });
192        }
193
194        /* Check if already fully resolved (by canonical location) */
195        if let Some(pkg_id) = state.location_to_package.get(&fetch_result.canonical_location) {
196            if self.verbose {
197                println!("    [~] Already resolved: {}", pkg_id);
198            }
199            return Ok(pkg_id.clone());
200        }
201
202        /* Parse the ABI file */
203        let abi_file: AbiFile =
204            serde_yml::from_str(&fetch_result.content).map_err(|e| ResolveError::ParseError {
205                location: fetch_result.canonical_location.clone(),
206                message: e.to_string(),
207            })?;
208
209        let pkg_id = PackageId::from_abi_file(&abi_file);
210
211        if self.verbose {
212            println!("    Package: {}", pkg_id);
213        }
214
215        /* Check for version conflict */
216        self.check_version_conflict(&pkg_id, state)?;
217
218        /* Mark as being resolved */
219        state.in_progress.insert(fetch_result.canonical_location.clone());
220        state.resolution_chain.push(pkg_id.clone());
221
222        /* Create context for resolving this file's imports:
223           - base_path: current file's resolved path (for relative path resolution)
224           - parent_is_remote: whether this file came from a remote source
225           - include_dirs: inherited from root context */
226        let import_ctx = FetchContext {
227            base_path: fetch_result.resolved_path.clone(),
228            parent_is_remote: fetch_result.is_remote,
229            include_dirs: ctx.include_dirs.clone(),
230        };
231
232        /* Resolve all imports recursively */
233        let mut dependencies = Vec::new();
234        for import in abi_file.imports() {
235            if self.verbose {
236                println!("    [~] Resolving import: {:?}", import);
237            }
238
239            let dep_id = self.resolve_import(import, &import_ctx, state)?;
240            dependencies.push(dep_id);
241        }
242
243        /* Create resolved package */
244        let resolved = ResolvedPackage {
245            id: pkg_id.clone(),
246            source: source.clone(),
247            abi_file,
248            dependencies,
249            is_remote: fetch_result.is_remote,
250        };
251
252        /* Mark as fully resolved */
253        state.in_progress.remove(&fetch_result.canonical_location);
254        state.resolution_chain.pop();
255        state.resolved_packages.insert(pkg_id.clone(), resolved);
256        state.location_to_package.insert(fetch_result.canonical_location, pkg_id.clone());
257        state.versions.insert(pkg_id.package_name.clone(), pkg_id.version.clone());
258
259        Ok(pkg_id)
260    }
261
262    /* Check for version conflicts */
263    fn check_version_conflict(
264        &self,
265        pkg_id: &PackageId,
266        state: &ResolutionState,
267    ) -> Result<(), ResolveError> {
268        if let Some(existing_version) = state.versions.get(&pkg_id.package_name) {
269            if existing_version != &pkg_id.version {
270                return Err(ResolveError::VersionConflict {
271                    package_name: pkg_id.package_name.clone(),
272                    version_a: existing_version.clone(),
273                    version_b: pkg_id.version.clone(),
274                });
275            }
276        }
277        Ok(())
278    }
279}
280
281/* ============================================================================
282   Resolution State (internal)
283   ============================================================================ */
284
285/* Internal state tracked during resolution */
286struct ResolutionState {
287    /* Packages currently being resolved (for cycle detection) */
288    in_progress: HashSet<String>,
289
290    /* Chain of packages being resolved (for error reporting) */
291    resolution_chain: Vec<PackageId>,
292
293    /* Fully resolved packages by PackageId */
294    resolved_packages: HashMap<PackageId, ResolvedPackage>,
295
296    /* Map from canonical location to PackageId */
297    location_to_package: HashMap<String, PackageId>,
298
299    /* Map from package name to resolved version (for conflict detection) */
300    versions: HashMap<String, String>,
301}
302
303impl ResolutionState {
304    fn new() -> Self {
305        Self {
306            in_progress: HashSet::new(),
307            resolution_chain: Vec::new(),
308            resolved_packages: HashMap::new(),
309            location_to_package: HashMap::new(),
310            versions: HashMap::new(),
311        }
312    }
313}
314
315/* ============================================================================
316   Tests
317   ============================================================================ */
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use std::io::Write;
323    use tempfile::TempDir;
324
325    fn create_test_abi(dir: &std::path::Path, name: &str, content: &str) -> PathBuf {
326        let path = dir.join(name);
327        let mut file = std::fs::File::create(&path).unwrap();
328        file.write_all(content.as_bytes()).unwrap();
329        path
330    }
331
332    #[test]
333    fn test_resolve_single_file() {
334        let temp_dir = TempDir::new().unwrap();
335        let abi_content = r#"
336abi:
337  package: "test.single"
338  abi-version: 1
339  package-version: "1.0.0"
340  description: "Single file test"
341types: []
342"#;
343        let abi_path = create_test_abi(temp_dir.path(), "single.abi.yaml", abi_content);
344
345        let resolver = EnhancedImportResolver::with_defaults(vec![]).unwrap();
346        let result = resolver.resolve_file(&abi_path).unwrap();
347
348        assert_eq!(result.root.package_name(), "test.single");
349        assert_eq!(result.package_count(), 1);
350    }
351
352    #[test]
353    fn test_resolve_with_imports() {
354        let temp_dir = TempDir::new().unwrap();
355
356        /* Create child ABI */
357        let child_content = r#"
358abi:
359  package: "test.child"
360  abi-version: 1
361  package-version: "1.0.0"
362  description: "Child package"
363types:
364  - name: "ChildType"
365    kind:
366      struct:
367        fields:
368          - name: "value"
369            field-type:
370              primitive: u32
371"#;
372        create_test_abi(temp_dir.path(), "child.abi.yaml", child_content);
373
374        /* Create parent ABI that imports child */
375        let parent_content = r#"
376abi:
377  package: "test.parent"
378  abi-version: 1
379  package-version: "1.0.0"
380  description: "Parent package"
381  imports:
382    - type: path
383      path: "child.abi.yaml"
384types:
385  - name: "ParentType"
386    kind:
387      struct:
388        fields:
389          - name: "child"
390            field-type:
391              type-ref:
392                name: ChildType
393"#;
394        let parent_path = create_test_abi(temp_dir.path(), "parent.abi.yaml", parent_content);
395
396        let resolver = EnhancedImportResolver::with_defaults(vec![]).unwrap();
397        let result = resolver.resolve_file(&parent_path).unwrap();
398
399        assert_eq!(result.root.package_name(), "test.parent");
400        assert_eq!(result.package_count(), 2);
401
402        /* Verify child was resolved */
403        let child_id = PackageId::new("test.child", "1.0.0");
404        assert!(result.get_package(&child_id).is_some());
405    }
406
407    #[test]
408    fn test_cycle_detection() {
409        let temp_dir = TempDir::new().unwrap();
410
411        /* Create ABI A that imports B */
412        let a_content = r#"
413abi:
414  package: "test.a"
415  abi-version: 1
416  package-version: "1.0.0"
417  description: "Package A"
418  imports:
419    - type: path
420      path: "b.abi.yaml"
421types: []
422"#;
423        create_test_abi(temp_dir.path(), "a.abi.yaml", a_content);
424
425        /* Create ABI B that imports A (cycle) */
426        let b_content = r#"
427abi:
428  package: "test.b"
429  abi-version: 1
430  package-version: "1.0.0"
431  description: "Package B"
432  imports:
433    - type: path
434      path: "a.abi.yaml"
435types: []
436"#;
437        create_test_abi(temp_dir.path(), "b.abi.yaml", b_content);
438
439        let a_path = temp_dir.path().join("a.abi.yaml");
440        let resolver = EnhancedImportResolver::with_defaults(vec![]).unwrap();
441        let result = resolver.resolve_file(&a_path);
442
443        assert!(matches!(result, Err(ResolveError::CyclicDependency { .. })));
444    }
445
446    #[test]
447    fn test_version_conflict_detection() {
448        let temp_dir = TempDir::new().unwrap();
449
450        /* Create two versions of the same package */
451        let common_v1 = r#"
452abi:
453  package: "test.common"
454  abi-version: 1
455  package-version: "1.0.0"
456  description: "Common v1"
457types: []
458"#;
459        create_test_abi(temp_dir.path(), "common_v1.abi.yaml", common_v1);
460
461        let common_v2 = r#"
462abi:
463  package: "test.common"
464  abi-version: 1
465  package-version: "2.0.0"
466  description: "Common v2"
467types: []
468"#;
469        create_test_abi(temp_dir.path(), "common_v2.abi.yaml", common_v2);
470
471        /* Create package A importing common v1 */
472        let a_content = r#"
473abi:
474  package: "test.a"
475  abi-version: 1
476  package-version: "1.0.0"
477  description: "Package A"
478  imports:
479    - type: path
480      path: "common_v1.abi.yaml"
481types: []
482"#;
483        create_test_abi(temp_dir.path(), "a.abi.yaml", a_content);
484
485        /* Create package B importing common v2 */
486        let b_content = r#"
487abi:
488  package: "test.b"
489  abi-version: 1
490  package-version: "1.0.0"
491  description: "Package B"
492  imports:
493    - type: path
494      path: "common_v2.abi.yaml"
495types: []
496"#;
497        create_test_abi(temp_dir.path(), "b.abi.yaml", b_content);
498
499        /* Create root importing both A and B */
500        let root_content = r#"
501abi:
502  package: "test.root"
503  abi-version: 1
504  package-version: "1.0.0"
505  description: "Root package"
506  imports:
507    - type: path
508      path: "a.abi.yaml"
509    - type: path
510      path: "b.abi.yaml"
511types: []
512"#;
513        let root_path = create_test_abi(temp_dir.path(), "root.abi.yaml", root_content);
514
515        let resolver = EnhancedImportResolver::with_defaults(vec![]).unwrap();
516        let result = resolver.resolve_file(&root_path);
517
518        assert!(matches!(
519            result,
520            Err(ResolveError::VersionConflict {
521                package_name,
522                ..
523            }) if package_name == "test.common"
524        ));
525    }
526
527    #[test]
528    fn test_duplicate_import_deduplication() {
529        let temp_dir = TempDir::new().unwrap();
530
531        /* Create common package */
532        let common_content = r#"
533abi:
534  package: "test.common"
535  abi-version: 1
536  package-version: "1.0.0"
537  description: "Common package"
538types: []
539"#;
540        create_test_abi(temp_dir.path(), "common.abi.yaml", common_content);
541
542        /* Create A importing common */
543        let a_content = r#"
544abi:
545  package: "test.a"
546  abi-version: 1
547  package-version: "1.0.0"
548  description: "Package A"
549  imports:
550    - type: path
551      path: "common.abi.yaml"
552types: []
553"#;
554        create_test_abi(temp_dir.path(), "a.abi.yaml", a_content);
555
556        /* Create B importing common */
557        let b_content = r#"
558abi:
559  package: "test.b"
560  abi-version: 1
561  package-version: "1.0.0"
562  description: "Package B"
563  imports:
564    - type: path
565      path: "common.abi.yaml"
566types: []
567"#;
568        create_test_abi(temp_dir.path(), "b.abi.yaml", b_content);
569
570        /* Create root importing both A and B (common imported twice, same version) */
571        let root_content = r#"
572abi:
573  package: "test.root"
574  abi-version: 1
575  package-version: "1.0.0"
576  description: "Root package"
577  imports:
578    - type: path
579      path: "a.abi.yaml"
580    - type: path
581      path: "b.abi.yaml"
582types: []
583"#;
584        let root_path = create_test_abi(temp_dir.path(), "root.abi.yaml", root_content);
585
586        let resolver = EnhancedImportResolver::with_defaults(vec![]).unwrap();
587        let result = resolver.resolve_file(&root_path).unwrap();
588
589        /* Should have 4 packages: root, a, b, common (common only once) */
590        assert_eq!(result.package_count(), 4);
591
592        /* Verify common appears only once */
593        let common_count = result
594            .all_packages
595            .iter()
596            .filter(|p| p.package_name() == "test.common")
597            .count();
598        assert_eq!(common_count, 1);
599    }
600
601    #[test]
602    fn test_to_manifest() {
603        let temp_dir = TempDir::new().unwrap();
604        let abi_content = r#"
605abi:
606  package: "test.manifest"
607  abi-version: 1
608  package-version: "1.0.0"
609  description: "Manifest test"
610types:
611  - name: "TestType"
612    kind:
613      struct:
614        fields:
615          - name: "value"
616            field-type:
617              primitive: u32
618"#;
619        let abi_path = create_test_abi(temp_dir.path(), "manifest.abi.yaml", abi_content);
620
621        let resolver = EnhancedImportResolver::with_defaults(vec![]).unwrap();
622        let result = resolver.resolve_file(&abi_path).unwrap();
623
624        let manifest = result.to_manifest();
625        assert_eq!(manifest.len(), 1);
626        assert!(manifest.contains_key("test.manifest"));
627        assert!(manifest.get("test.manifest").unwrap().contains("TestType"));
628    }
629
630    #[test]
631    fn test_local_only_config() {
632        let temp_dir = TempDir::new().unwrap();
633        let abi_content = r#"
634abi:
635  package: "test.local"
636  abi-version: 1
637  package-version: "1.0.0"
638  description: "Local only test"
639types: []
640"#;
641        let abi_path = create_test_abi(temp_dir.path(), "local.abi.yaml", abi_content);
642
643        /* Use local_only config */
644        let resolver = EnhancedImportResolver::new(FetcherConfig::local_only(), vec![]).unwrap();
645        let result = resolver.resolve_file(&abi_path).unwrap();
646
647        assert_eq!(result.root.package_name(), "test.local");
648    }
649}