1use 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
14pub struct EnhancedImportResolver {
20 fetcher: CompositeFetcher,
22
23 include_dirs: Vec<PathBuf>,
25
26 verbose: bool,
28}
29
30impl EnhancedImportResolver {
31 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 pub fn with_defaults(include_dirs: Vec<PathBuf>) -> Result<Self, ResolveError> {
45 Self::new(FetcherConfig::cli_default(), include_dirs)
46 }
47
48 pub fn with_verbose(mut self, verbose: bool) -> Self {
50 self.verbose = verbose;
51 self
52 }
53
54 pub fn config(&self) -> &FetcherConfig {
56 self.fetcher.config()
57 }
58
59 pub fn resolve_file(&self, file_path: &PathBuf) -> Result<ResolutionResult, ResolveError> {
61 let root_source = ImportSource::Path {
63 path: file_path.to_string_lossy().to_string(),
64 };
65
66 let root_ctx = FetchContext::for_root(Some(file_path.clone()), self.include_dirs.clone());
68
69 let mut state = ResolutionState::new();
71
72 let root_id = self.resolve_import(&root_source, &root_ctx, &mut state)?;
74
75 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 pub fn resolve_content(
93 &self,
94 content: &str,
95 canonical_location: &str,
96 ) -> Result<ResolutionResult, ResolveError> {
97 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 let root_source = ImportSource::Path {
106 path: canonical_location.to_string(),
107 };
108
109 let mut state = ResolutionState::new();
111
112 let root_ctx = FetchContext::for_root(None, self.include_dirs.clone());
114
115 let pkg_id = PackageId::from_abi_file(&abi_file);
117
118 self.check_version_conflict(&pkg_id, &state)?;
120
121 state.in_progress.insert(canonical_location.to_string());
123 state.resolution_chain.push(pkg_id.clone());
124
125 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 let resolved = ResolvedPackage::new(root_source.clone(), abi_file, dependencies);
135
136 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 fn resolve_import(
154 &self,
155 source: &ImportSource,
156 ctx: &FetchContext,
157 state: &mut ResolutionState,
158 ) -> Result<PackageId, ResolveError> {
159 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 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 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 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 self.check_version_conflict(&pkg_id, state)?;
226
227 state
229 .in_progress
230 .insert(fetch_result.canonical_location.clone());
231 state.resolution_chain.push(pkg_id.clone());
232
233 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 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 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 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 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
296struct ResolutionState {
302 in_progress: HashSet<String>,
304
305 resolution_chain: Vec<PackageId>,
307
308 resolved_packages: HashMap<PackageId, ResolvedPackage>,
310
311 location_to_package: HashMap<String, PackageId>,
313
314 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(result.package_count(), 4);
606
607 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 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}