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)
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 pub fn with_defaults(include_dirs: Vec<PathBuf>) -> Result<Self, ResolveError> {
44 Self::new(FetcherConfig::cli_default(), include_dirs)
45 }
46
47 pub fn with_verbose(mut self, verbose: bool) -> Self {
49 self.verbose = verbose;
50 self
51 }
52
53 pub fn config(&self) -> &FetcherConfig {
55 self.fetcher.config()
56 }
57
58 pub fn resolve_file(&self, file_path: &PathBuf) -> Result<ResolutionResult, ResolveError> {
60 let root_source = ImportSource::Path {
62 path: file_path.to_string_lossy().to_string(),
63 };
64
65 let root_ctx = FetchContext::for_root(Some(file_path.clone()), self.include_dirs.clone());
67
68 let mut state = ResolutionState::new();
70
71 let root_id = self.resolve_import(&root_source, &root_ctx, &mut state)?;
73
74 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 pub fn resolve_content(
92 &self,
93 content: &str,
94 canonical_location: &str,
95 ) -> Result<ResolutionResult, ResolveError> {
96 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 let root_source = ImportSource::Path {
104 path: canonical_location.to_string(),
105 };
106
107 let mut state = ResolutionState::new();
109
110 let root_ctx = FetchContext::for_root(None, self.include_dirs.clone());
112
113 let pkg_id = PackageId::from_abi_file(&abi_file);
115
116 self.check_version_conflict(&pkg_id, &state)?;
118
119 state.in_progress.insert(canonical_location.to_string());
121 state.resolution_chain.push(pkg_id.clone());
122
123 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 let resolved = ResolvedPackage::new(root_source.clone(), abi_file, dependencies);
133
134 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 fn resolve_import(
148 &self,
149 source: &ImportSource,
150 ctx: &FetchContext,
151 state: &mut ResolutionState,
152 ) -> Result<PackageId, ResolveError> {
153 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 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 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 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 self.check_version_conflict(&pkg_id, state)?;
217
218 state.in_progress.insert(fetch_result.canonical_location.clone());
220 state.resolution_chain.push(pkg_id.clone());
221
222 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 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 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 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 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
281struct ResolutionState {
287 in_progress: HashSet<String>,
289
290 resolution_chain: Vec<PackageId>,
292
293 resolved_packages: HashMap<PackageId, ResolvedPackage>,
295
296 location_to_package: HashMap<String, PackageId>,
298
299 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(result.package_count(), 4);
591
592 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 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}