Skip to main content

torsh_package/
dependency.rs

1//! Dependency resolution and management for packages
2//!
3//! This module provides functionality for resolving package dependencies,
4//! handling version conflicts, and automatically installing required packages.
5
6// Framework infrastructure - components designed for future use
7#![allow(dead_code)]
8use std::collections::{HashMap, HashSet, VecDeque};
9use std::path::Path;
10
11use serde::{Deserialize, Serialize};
12use torsh_core::error::{Result, TorshError};
13
14use crate::package::Package;
15use crate::version::{PackageVersion, VersionRequirement};
16
17/// Dependency specification with version requirements
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19pub struct DependencySpec {
20    /// Name of the dependency package
21    pub name: String,
22    /// Version requirement (e.g., "^1.0.0", ">=2.0.0")
23    pub version_req: String,
24    /// Optional features to enable
25    pub features: Vec<String>,
26    /// Whether this dependency is optional
27    pub optional: bool,
28    /// Platform-specific requirements
29    pub platform: Option<String>,
30}
31
32/// Resolved dependency with specific version
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ResolvedDependency {
35    /// Dependency specification
36    pub spec: DependencySpec,
37    /// Resolved version
38    pub resolved_version: String,
39    /// Path to the package file
40    pub package_path: Option<String>,
41    /// Transitive dependencies
42    pub dependencies: Vec<ResolvedDependency>,
43}
44
45/// Dependency resolution strategy
46#[derive(Debug, Clone, Copy)]
47pub enum ResolutionStrategy {
48    /// Use the highest compatible version
49    Highest,
50    /// Use the lowest compatible version
51    Lowest,
52    /// Use the most recent stable version
53    Stable,
54}
55
56/// Dependency resolver for handling complex dependency graphs
57pub struct DependencyResolver {
58    /// Resolution strategy to use
59    strategy: ResolutionStrategy,
60    /// Package registry to search for dependencies
61    registry: Box<dyn PackageRegistry>,
62    /// Maximum dependency depth to prevent infinite loops
63    max_depth: usize,
64    /// Enable parallel resolution
65    parallel_resolution: bool,
66}
67
68/// Package registry trait for abstracting package sources
69pub trait PackageRegistry: Send + Sync {
70    /// Search for packages matching the given name pattern
71    fn search_packages(&self, name_pattern: &str) -> Result<Vec<PackageInfo>>;
72
73    /// Get available versions for a package
74    fn get_versions(&self, package_name: &str) -> Result<Vec<String>>;
75
76    /// Download a specific package version
77    fn download_package(&self, name: &str, version: &str, dest_path: &Path) -> Result<()>;
78
79    /// Get package metadata without downloading
80    fn get_package_info(&self, name: &str, version: &str) -> Result<PackageInfo>;
81}
82
83/// Package information from registry
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct PackageInfo {
86    /// Package name
87    pub name: String,
88    /// Package version
89    pub version: String,
90    /// Package description
91    pub description: Option<String>,
92    /// Package author
93    pub author: Option<String>,
94    /// Package dependencies
95    pub dependencies: Vec<DependencySpec>,
96    /// Package size in bytes
97    pub size: u64,
98    /// Package checksum
99    pub checksum: String,
100    /// Registry URL where package can be found
101    pub registry_url: String,
102}
103
104/// Dependency conflict information
105#[derive(Debug, Clone)]
106pub struct DependencyConflict {
107    /// Package name with conflict
108    pub package_name: String,
109    /// Conflicting version requirements
110    pub conflicts: Vec<(String, String)>, // (source, requirement)
111    /// Suggested resolution
112    pub suggestion: Option<String>,
113}
114
115/// Dependency graph for analyzing relationships
116#[derive(Debug, Clone)]
117pub struct DependencyGraph {
118    /// Nodes in the graph (package name -> package info)
119    nodes: HashMap<String, PackageInfo>,
120    /// Edges in the graph (dependent -> [dependencies])
121    edges: HashMap<String, Vec<String>>,
122    /// Resolved versions
123    resolved_versions: HashMap<String, String>,
124}
125
126impl DependencySpec {
127    /// Create a new dependency specification
128    pub fn new(name: String, version_req: String) -> Self {
129        Self {
130            name,
131            version_req,
132            features: Vec::new(),
133            optional: false,
134            platform: None,
135        }
136    }
137
138    /// Add a feature requirement
139    pub fn with_feature(mut self, feature: String) -> Self {
140        self.features.push(feature);
141        self
142    }
143
144    /// Mark as optional dependency
145    pub fn optional(mut self) -> Self {
146        self.optional = true;
147        self
148    }
149
150    /// Add platform requirement
151    pub fn for_platform(mut self, platform: String) -> Self {
152        self.platform = Some(platform);
153        self
154    }
155
156    /// Check if this dependency is compatible with the current platform
157    pub fn is_compatible_platform(&self) -> bool {
158        // Simplified platform check - in production, you'd check actual platform
159        self.platform
160            .as_ref()
161            .map_or(true, |p| p == "any" || p == std::env::consts::OS)
162    }
163
164    /// Check if a version satisfies this dependency requirement
165    pub fn is_satisfied_by(&self, version: &str) -> Result<bool> {
166        let requirement = VersionRequirement::parse(&self.version_req).map_err(|e| {
167            TorshError::InvalidArgument(format!("Invalid version requirement: {}", e))
168        })?;
169        let package_version = PackageVersion::parse(version)
170            .map_err(|e| TorshError::InvalidArgument(format!("Invalid version: {}", e)))?;
171        Ok(requirement.matches(&package_version))
172    }
173}
174
175impl Default for DependencyResolver {
176    fn default() -> Self {
177        Self::new(Box::new(LocalPackageRegistry::default()))
178    }
179}
180
181impl DependencyResolver {
182    /// Create a new dependency resolver
183    pub fn new(registry: Box<dyn PackageRegistry>) -> Self {
184        Self {
185            strategy: ResolutionStrategy::Highest,
186            registry,
187            max_depth: 100,
188            parallel_resolution: false,
189        }
190    }
191
192    /// Set resolution strategy
193    pub fn with_strategy(mut self, strategy: ResolutionStrategy) -> Self {
194        self.strategy = strategy;
195        self
196    }
197
198    /// Set maximum dependency depth
199    pub fn with_max_depth(mut self, max_depth: usize) -> Self {
200        self.max_depth = max_depth;
201        self
202    }
203
204    /// Enable parallel resolution
205    pub fn with_parallel_resolution(mut self, parallel: bool) -> Self {
206        self.parallel_resolution = parallel;
207        self
208    }
209
210    /// Resolve dependencies for a package
211    pub fn resolve_dependencies(&self, package: &Package) -> Result<Vec<ResolvedDependency>> {
212        let mut resolved = Vec::new();
213        let mut visited = HashSet::new();
214        let mut queue = VecDeque::new();
215
216        // Start with direct dependencies
217        for (name, version_req) in &package.manifest.dependencies {
218            let spec = DependencySpec::new(name.clone(), version_req.clone());
219            queue.push_back((spec, 0)); // (spec, depth)
220        }
221
222        while let Some((spec, depth)) = queue.pop_front() {
223            if depth >= self.max_depth {
224                return Err(TorshError::InvalidArgument(format!(
225                    "Maximum dependency depth exceeded for package: {}",
226                    spec.name
227                )));
228            }
229
230            if visited.contains(&spec.name) {
231                continue;
232            }
233
234            if !spec.is_compatible_platform() {
235                continue; // Skip platform-incompatible dependencies
236            }
237
238            // Resolve the specific version
239            let resolved_version = self.resolve_version(&spec)?;
240            let package_info = self
241                .registry
242                .get_package_info(&spec.name, &resolved_version)?;
243
244            // Add transitive dependencies to queue
245            for dep in &package_info.dependencies {
246                if !visited.contains(&dep.name) && !dep.optional {
247                    queue.push_back((dep.clone(), depth + 1));
248                }
249            }
250
251            let resolved_dep = ResolvedDependency {
252                spec: spec.clone(),
253                resolved_version,
254                package_path: None,       // Will be set during installation
255                dependencies: Vec::new(), // Will be populated recursively
256            };
257
258            resolved.push(resolved_dep);
259            visited.insert(spec.name.clone());
260        }
261
262        // Check for conflicts
263        self.check_conflicts(&resolved)?;
264
265        Ok(resolved)
266    }
267
268    /// Resolve a specific version for a dependency
269    fn resolve_version(&self, spec: &DependencySpec) -> Result<String> {
270        let available_versions = self.registry.get_versions(&spec.name)?;
271
272        if available_versions.is_empty() {
273            return Err(TorshError::InvalidArgument(format!(
274                "No versions found for package: {}",
275                spec.name
276            )));
277        }
278
279        // Filter compatible versions
280        let mut compatible_versions = Vec::new();
281        for version in &available_versions {
282            if spec.is_satisfied_by(version)? {
283                compatible_versions.push(version.clone());
284            }
285        }
286
287        if compatible_versions.is_empty() {
288            return Err(TorshError::InvalidArgument(format!(
289                "No compatible versions found for package: {} with requirement: {}",
290                spec.name, spec.version_req
291            )));
292        }
293
294        // Apply resolution strategy
295        let selected_version = match self.strategy {
296            ResolutionStrategy::Highest => self.select_highest_version(&compatible_versions)?,
297            ResolutionStrategy::Lowest => self.select_lowest_version(&compatible_versions)?,
298            ResolutionStrategy::Stable => self.select_stable_version(&compatible_versions)?,
299        };
300
301        Ok(selected_version)
302    }
303
304    /// Select the highest compatible version
305    fn select_highest_version(&self, versions: &[String]) -> Result<String> {
306        let mut parsed_versions: Vec<_> = versions
307            .iter()
308            .map(|v| (v, PackageVersion::parse(v)))
309            .filter_map(|(v, parsed)| parsed.ok().map(|p| (v.clone(), p)))
310            .collect();
311
312        parsed_versions.sort_by(|a, b| b.1.cmp(&a.1));
313
314        parsed_versions
315            .first()
316            .map(|(version, _)| version.clone())
317            .ok_or_else(|| TorshError::InvalidArgument("No valid versions found".to_string()))
318    }
319
320    /// Select the lowest compatible version
321    fn select_lowest_version(&self, versions: &[String]) -> Result<String> {
322        let mut parsed_versions: Vec<_> = versions
323            .iter()
324            .map(|v| (v, PackageVersion::parse(v)))
325            .filter_map(|(v, parsed)| parsed.ok().map(|p| (v.clone(), p)))
326            .collect();
327
328        parsed_versions.sort_by(|a, b| a.1.cmp(&b.1));
329
330        parsed_versions
331            .first()
332            .map(|(version, _)| version.clone())
333            .ok_or_else(|| TorshError::InvalidArgument("No valid versions found".to_string()))
334    }
335
336    /// Select the most recent stable version (non-prerelease)
337    fn select_stable_version(&self, versions: &[String]) -> Result<String> {
338        let mut stable_versions: Vec<_> = versions
339            .iter()
340            .map(|v| (v, PackageVersion::parse(v)))
341            .filter_map(|(v, parsed)| {
342                parsed.ok().and_then(|p| {
343                    if p.pre_release.is_none() {
344                        // No prerelease
345                        Some((v.clone(), p))
346                    } else {
347                        None
348                    }
349                })
350            })
351            .collect();
352
353        if stable_versions.is_empty() {
354            // Fall back to highest version if no stable versions available
355            return self.select_highest_version(versions);
356        }
357
358        stable_versions.sort_by(|a, b| b.1.cmp(&a.1));
359
360        stable_versions
361            .first()
362            .map(|(version, _)| version.clone())
363            .ok_or_else(|| TorshError::InvalidArgument("No stable versions found".to_string()))
364    }
365
366    /// Check for dependency conflicts
367    fn check_conflicts(&self, resolved: &[ResolvedDependency]) -> Result<()> {
368        let mut package_versions: HashMap<String, Vec<String>> = HashMap::new();
369
370        for dep in resolved {
371            package_versions
372                .entry(dep.spec.name.clone())
373                .or_default()
374                .push(dep.resolved_version.clone());
375        }
376
377        let mut conflicts = Vec::new();
378        for (package_name, versions) in &package_versions {
379            let unique_versions: HashSet<_> = versions.iter().collect();
380            if unique_versions.len() > 1 {
381                let conflict = DependencyConflict {
382                    package_name: package_name.clone(),
383                    conflicts: versions
384                        .iter()
385                        .map(|v| (package_name.clone(), v.clone()))
386                        .collect(),
387                    suggestion: Some(format!("Use version {}", versions[0])),
388                };
389                conflicts.push(conflict);
390            }
391        }
392
393        if !conflicts.is_empty() {
394            let conflict_descriptions: Vec<String> = conflicts
395                .iter()
396                .map(|c| {
397                    format!(
398                        "Package '{}' has conflicting version requirements",
399                        c.package_name
400                    )
401                })
402                .collect();
403
404            return Err(TorshError::InvalidArgument(format!(
405                "Dependency conflicts detected: {}",
406                conflict_descriptions.join(", ")
407            )));
408        }
409
410        Ok(())
411    }
412
413    /// Install resolved dependencies
414    pub fn install_dependencies(
415        &self,
416        resolved: &mut [ResolvedDependency],
417        install_dir: &Path,
418    ) -> Result<()> {
419        for dep in resolved {
420            let package_path = install_dir.join(format!(
421                "{}-{}.torshpkg",
422                dep.spec.name, dep.resolved_version
423            ));
424
425            // Download the package
426            self.registry
427                .download_package(&dep.spec.name, &dep.resolved_version, &package_path)?;
428
429            // Update the package path
430            dep.package_path = Some(package_path.to_string_lossy().to_string());
431        }
432
433        Ok(())
434    }
435
436    /// Build dependency graph for analysis
437    pub fn build_dependency_graph(&self, package: &Package) -> Result<DependencyGraph> {
438        let resolved = self.resolve_dependencies(package)?;
439        let mut graph = DependencyGraph::new();
440
441        for dep in &resolved {
442            let package_info = self
443                .registry
444                .get_package_info(&dep.spec.name, &dep.resolved_version)?;
445            graph.add_package(package_info);
446        }
447
448        Ok(graph)
449    }
450}
451
452impl DependencyGraph {
453    /// Create a new empty dependency graph
454    pub fn new() -> Self {
455        Self {
456            nodes: HashMap::new(),
457            edges: HashMap::new(),
458            resolved_versions: HashMap::new(),
459        }
460    }
461
462    /// Add a package to the graph
463    pub fn add_package(&mut self, package_info: PackageInfo) {
464        let package_name = package_info.name.clone();
465        self.resolved_versions
466            .insert(package_name.clone(), package_info.version.clone());
467
468        // Add dependencies as edges
469        let mut dependencies = Vec::new();
470        for dep in &package_info.dependencies {
471            dependencies.push(dep.name.clone());
472        }
473        self.edges.insert(package_name.clone(), dependencies);
474
475        self.nodes.insert(package_name, package_info);
476    }
477
478    /// Get topological ordering of dependencies
479    pub fn topological_sort(&self) -> Result<Vec<String>> {
480        let mut result = Vec::new();
481        let mut visited = HashSet::new();
482        let mut in_stack = HashSet::new();
483
484        for package_name in self.nodes.keys() {
485            if !visited.contains(package_name) {
486                self.topological_sort_util(package_name, &mut visited, &mut in_stack, &mut result)?;
487            }
488        }
489
490        result.reverse();
491        Ok(result)
492    }
493
494    /// Utility function for topological sort
495    fn topological_sort_util(
496        &self,
497        package_name: &str,
498        visited: &mut HashSet<String>,
499        in_stack: &mut HashSet<String>,
500        result: &mut Vec<String>,
501    ) -> Result<()> {
502        if in_stack.contains(package_name) {
503            return Err(TorshError::InvalidArgument(format!(
504                "Circular dependency detected involving package: {}",
505                package_name
506            )));
507        }
508
509        if visited.contains(package_name) {
510            return Ok(());
511        }
512
513        visited.insert(package_name.to_string());
514        in_stack.insert(package_name.to_string());
515
516        if let Some(dependencies) = self.edges.get(package_name) {
517            for dep in dependencies {
518                self.topological_sort_util(dep, visited, in_stack, result)?;
519            }
520        }
521
522        in_stack.remove(package_name);
523        result.push(package_name.to_string());
524        Ok(())
525    }
526
527    /// Get all packages in the graph
528    pub fn get_packages(&self) -> &HashMap<String, PackageInfo> {
529        &self.nodes
530    }
531
532    /// Get dependencies for a package
533    pub fn get_dependencies(&self, package_name: &str) -> Option<&Vec<String>> {
534        self.edges.get(package_name)
535    }
536}
537
538/// Local package registry implementation (for testing and local development)
539#[derive(Debug, Default)]
540pub struct LocalPackageRegistry {
541    /// Local package cache directory
542    cache_dir: Option<String>,
543    /// Available packages
544    packages: HashMap<String, Vec<PackageInfo>>,
545}
546
547impl LocalPackageRegistry {
548    /// Create a new local registry
549    pub fn new() -> Self {
550        Self::default()
551    }
552
553    /// Add a package to the local registry
554    pub fn add_package(&mut self, package_info: PackageInfo) {
555        self.packages
556            .entry(package_info.name.clone())
557            .or_default()
558            .push(package_info);
559    }
560}
561
562impl PackageRegistry for LocalPackageRegistry {
563    fn search_packages(&self, name_pattern: &str) -> Result<Vec<PackageInfo>> {
564        let mut results = Vec::new();
565
566        for (name, packages) in &self.packages {
567            if name.contains(name_pattern) {
568                results.extend(packages.iter().cloned());
569            }
570        }
571
572        Ok(results)
573    }
574
575    fn get_versions(&self, package_name: &str) -> Result<Vec<String>> {
576        let versions = self
577            .packages
578            .get(package_name)
579            .map(|packages| packages.iter().map(|p| p.version.clone()).collect())
580            .unwrap_or_default();
581
582        Ok(versions)
583    }
584
585    fn download_package(&self, _name: &str, _version: &str, _dest_path: &Path) -> Result<()> {
586        // Simulate download - in a real implementation, this would download from a registry
587        Ok(())
588    }
589
590    fn get_package_info(&self, name: &str, version: &str) -> Result<PackageInfo> {
591        let packages = self
592            .packages
593            .get(name)
594            .ok_or_else(|| TorshError::InvalidArgument(format!("Package not found: {}", name)))?;
595
596        packages
597            .iter()
598            .find(|p| p.version == version)
599            .cloned()
600            .ok_or_else(|| {
601                TorshError::InvalidArgument(format!(
602                    "Version {} not found for package: {}",
603                    version, name
604                ))
605            })
606    }
607}
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612
613    fn create_test_package_info(name: &str, version: &str) -> PackageInfo {
614        PackageInfo {
615            name: name.to_string(),
616            version: version.to_string(),
617            description: None,
618            author: None,
619            dependencies: Vec::new(),
620            size: 1024,
621            checksum: "abc123".to_string(),
622            registry_url: "http://localhost".to_string(),
623        }
624    }
625
626    #[test]
627    fn test_dependency_spec_creation() {
628        let spec = DependencySpec::new("test".to_string(), "^1.0.0".to_string())
629            .with_feature("test-feature".to_string())
630            .optional()
631            .for_platform("linux".to_string());
632
633        assert_eq!(spec.name, "test");
634        assert_eq!(spec.version_req, "^1.0.0");
635        assert_eq!(spec.features, vec!["test-feature"]);
636        assert!(spec.optional);
637        assert_eq!(spec.platform, Some("linux".to_string()));
638    }
639
640    #[test]
641    fn test_dependency_spec_version_satisfaction() {
642        let spec = DependencySpec::new("test".to_string(), "^1.0.0".to_string());
643
644        assert!(spec.is_satisfied_by("1.0.0").unwrap());
645        assert!(spec.is_satisfied_by("1.5.0").unwrap());
646        assert!(!spec.is_satisfied_by("2.0.0").unwrap());
647        assert!(!spec.is_satisfied_by("0.9.0").unwrap());
648    }
649
650    #[test]
651    fn test_local_package_registry() {
652        let mut registry = LocalPackageRegistry::new();
653        let package_info = create_test_package_info("test-package", "1.0.0");
654
655        registry.add_package(package_info.clone());
656
657        let versions = registry.get_versions("test-package").unwrap();
658        assert_eq!(versions, vec!["1.0.0"]);
659
660        let retrieved_info = registry.get_package_info("test-package", "1.0.0").unwrap();
661        assert_eq!(retrieved_info.name, package_info.name);
662        assert_eq!(retrieved_info.version, package_info.version);
663    }
664
665    #[test]
666    fn test_dependency_resolution_strategy() {
667        let registry = Box::new(LocalPackageRegistry::new());
668        let resolver = DependencyResolver::new(registry)
669            .with_strategy(ResolutionStrategy::Highest)
670            .with_max_depth(50);
671
672        // Test that strategy is set correctly
673        match resolver.strategy {
674            ResolutionStrategy::Highest => (),
675            _ => panic!("Strategy not set correctly"),
676        }
677
678        assert_eq!(resolver.max_depth, 50);
679    }
680
681    #[test]
682    fn test_dependency_graph() {
683        let mut graph = DependencyGraph::new();
684        let package_info = create_test_package_info("test-package", "1.0.0");
685
686        graph.add_package(package_info.clone());
687
688        assert_eq!(graph.nodes.len(), 1);
689        assert!(graph.nodes.contains_key("test-package"));
690    }
691
692    #[test]
693    fn test_version_selection() {
694        let resolver = DependencyResolver::default();
695        let versions = vec![
696            "1.0.0".to_string(),
697            "1.5.0".to_string(),
698            "2.0.0".to_string(),
699        ];
700
701        let highest = resolver.select_highest_version(&versions).unwrap();
702        assert_eq!(highest, "2.0.0");
703
704        let lowest = resolver.select_lowest_version(&versions).unwrap();
705        assert_eq!(lowest, "1.0.0");
706    }
707}