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