#![allow(dead_code)]
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::Path;
use serde::{Deserialize, Serialize};
use torsh_core::error::{Result, TorshError};
use crate::package::Package;
use crate::version::{PackageVersion, VersionRequirement};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DependencySpec {
pub name: String,
pub version_req: String,
pub features: Vec<String>,
pub optional: bool,
pub platform: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedDependency {
pub spec: DependencySpec,
pub resolved_version: String,
pub package_path: Option<String>,
pub dependencies: Vec<ResolvedDependency>,
}
#[derive(Debug, Clone, Copy)]
pub enum ResolutionStrategy {
Highest,
Lowest,
Stable,
}
pub struct DependencyResolver {
strategy: ResolutionStrategy,
registry: Box<dyn PackageRegistry>,
max_depth: usize,
parallel_resolution: bool,
}
pub trait PackageRegistry: Send + Sync {
fn search_packages(&self, name_pattern: &str) -> Result<Vec<PackageInfo>>;
fn get_versions(&self, package_name: &str) -> Result<Vec<String>>;
fn download_package(&self, name: &str, version: &str, dest_path: &Path) -> Result<()>;
fn get_package_info(&self, name: &str, version: &str) -> Result<PackageInfo>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageInfo {
pub name: String,
pub version: String,
pub description: Option<String>,
pub author: Option<String>,
pub dependencies: Vec<DependencySpec>,
pub size: u64,
pub checksum: String,
pub registry_url: String,
}
#[derive(Debug, Clone)]
pub struct DependencyConflict {
pub package_name: String,
pub conflicts: Vec<(String, String)>, pub suggestion: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DependencyGraph {
nodes: HashMap<String, PackageInfo>,
edges: HashMap<String, Vec<String>>,
resolved_versions: HashMap<String, String>,
}
impl DependencySpec {
pub fn new(name: String, version_req: String) -> Self {
Self {
name,
version_req,
features: Vec::new(),
optional: false,
platform: None,
}
}
pub fn with_feature(mut self, feature: String) -> Self {
self.features.push(feature);
self
}
pub fn optional(mut self) -> Self {
self.optional = true;
self
}
pub fn for_platform(mut self, platform: String) -> Self {
self.platform = Some(platform);
self
}
pub fn is_compatible_platform(&self) -> bool {
self.platform
.as_ref()
.map_or(true, |p| p == "any" || p == std::env::consts::OS)
}
pub fn is_satisfied_by(&self, version: &str) -> Result<bool> {
let requirement = VersionRequirement::parse(&self.version_req).map_err(|e| {
TorshError::InvalidArgument(format!("Invalid version requirement: {}", e))
})?;
let package_version = PackageVersion::parse(version)
.map_err(|e| TorshError::InvalidArgument(format!("Invalid version: {}", e)))?;
Ok(requirement.matches(&package_version))
}
}
impl Default for DependencyResolver {
fn default() -> Self {
Self::new(Box::new(LocalPackageRegistry::default()))
}
}
impl DependencyResolver {
pub fn new(registry: Box<dyn PackageRegistry>) -> Self {
Self {
strategy: ResolutionStrategy::Highest,
registry,
max_depth: 100,
parallel_resolution: false,
}
}
pub fn with_strategy(mut self, strategy: ResolutionStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn with_max_depth(mut self, max_depth: usize) -> Self {
self.max_depth = max_depth;
self
}
pub fn with_parallel_resolution(mut self, parallel: bool) -> Self {
self.parallel_resolution = parallel;
self
}
pub fn resolve_dependencies(&self, package: &Package) -> Result<Vec<ResolvedDependency>> {
let mut resolved = Vec::new();
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
for (name, version_req) in &package.manifest.dependencies {
let spec = DependencySpec::new(name.clone(), version_req.clone());
queue.push_back((spec, 0)); }
while let Some((spec, depth)) = queue.pop_front() {
if depth >= self.max_depth {
return Err(TorshError::InvalidArgument(format!(
"Maximum dependency depth exceeded for package: {}",
spec.name
)));
}
if visited.contains(&spec.name) {
continue;
}
if !spec.is_compatible_platform() {
continue; }
let resolved_version = self.resolve_version(&spec)?;
let package_info = self
.registry
.get_package_info(&spec.name, &resolved_version)?;
for dep in &package_info.dependencies {
if !visited.contains(&dep.name) && !dep.optional {
queue.push_back((dep.clone(), depth + 1));
}
}
let resolved_dep = ResolvedDependency {
spec: spec.clone(),
resolved_version,
package_path: None, dependencies: Vec::new(), };
resolved.push(resolved_dep);
visited.insert(spec.name.clone());
}
self.check_conflicts(&resolved)?;
Ok(resolved)
}
fn resolve_version(&self, spec: &DependencySpec) -> Result<String> {
let available_versions = self.registry.get_versions(&spec.name)?;
if available_versions.is_empty() {
return Err(TorshError::InvalidArgument(format!(
"No versions found for package: {}",
spec.name
)));
}
let mut compatible_versions = Vec::new();
for version in &available_versions {
if spec.is_satisfied_by(version)? {
compatible_versions.push(version.clone());
}
}
if compatible_versions.is_empty() {
return Err(TorshError::InvalidArgument(format!(
"No compatible versions found for package: {} with requirement: {}",
spec.name, spec.version_req
)));
}
let selected_version = match self.strategy {
ResolutionStrategy::Highest => self.select_highest_version(&compatible_versions)?,
ResolutionStrategy::Lowest => self.select_lowest_version(&compatible_versions)?,
ResolutionStrategy::Stable => self.select_stable_version(&compatible_versions)?,
};
Ok(selected_version)
}
fn select_highest_version(&self, versions: &[String]) -> Result<String> {
let mut parsed_versions: Vec<_> = versions
.iter()
.map(|v| (v, PackageVersion::parse(v)))
.filter_map(|(v, parsed)| parsed.ok().map(|p| (v.clone(), p)))
.collect();
parsed_versions.sort_by(|a, b| b.1.cmp(&a.1));
parsed_versions
.first()
.map(|(version, _)| version.clone())
.ok_or_else(|| TorshError::InvalidArgument("No valid versions found".to_string()))
}
fn select_lowest_version(&self, versions: &[String]) -> Result<String> {
let mut parsed_versions: Vec<_> = versions
.iter()
.map(|v| (v, PackageVersion::parse(v)))
.filter_map(|(v, parsed)| parsed.ok().map(|p| (v.clone(), p)))
.collect();
parsed_versions.sort_by(|a, b| a.1.cmp(&b.1));
parsed_versions
.first()
.map(|(version, _)| version.clone())
.ok_or_else(|| TorshError::InvalidArgument("No valid versions found".to_string()))
}
fn select_stable_version(&self, versions: &[String]) -> Result<String> {
let mut stable_versions: Vec<_> = versions
.iter()
.map(|v| (v, PackageVersion::parse(v)))
.filter_map(|(v, parsed)| {
parsed.ok().and_then(|p| {
if p.pre_release.is_none() {
Some((v.clone(), p))
} else {
None
}
})
})
.collect();
if stable_versions.is_empty() {
return self.select_highest_version(versions);
}
stable_versions.sort_by(|a, b| b.1.cmp(&a.1));
stable_versions
.first()
.map(|(version, _)| version.clone())
.ok_or_else(|| TorshError::InvalidArgument("No stable versions found".to_string()))
}
fn check_conflicts(&self, resolved: &[ResolvedDependency]) -> Result<()> {
let mut package_versions: HashMap<String, Vec<String>> = HashMap::new();
for dep in resolved {
package_versions
.entry(dep.spec.name.clone())
.or_default()
.push(dep.resolved_version.clone());
}
let mut conflicts = Vec::new();
for (package_name, versions) in &package_versions {
let unique_versions: HashSet<_> = versions.iter().collect();
if unique_versions.len() > 1 {
let conflict = DependencyConflict {
package_name: package_name.clone(),
conflicts: versions
.iter()
.map(|v| (package_name.clone(), v.clone()))
.collect(),
suggestion: Some(format!("Use version {}", versions[0])),
};
conflicts.push(conflict);
}
}
if !conflicts.is_empty() {
let conflict_descriptions: Vec<String> = conflicts
.iter()
.map(|c| {
format!(
"Package '{}' has conflicting version requirements",
c.package_name
)
})
.collect();
return Err(TorshError::InvalidArgument(format!(
"Dependency conflicts detected: {}",
conflict_descriptions.join(", ")
)));
}
Ok(())
}
pub fn install_dependencies(
&self,
resolved: &mut [ResolvedDependency],
install_dir: &Path,
) -> Result<()> {
for dep in resolved {
let package_path = install_dir.join(format!(
"{}-{}.torshpkg",
dep.spec.name, dep.resolved_version
));
self.registry
.download_package(&dep.spec.name, &dep.resolved_version, &package_path)?;
dep.package_path = Some(package_path.to_string_lossy().to_string());
}
Ok(())
}
pub fn build_dependency_graph(&self, package: &Package) -> Result<DependencyGraph> {
let resolved = self.resolve_dependencies(package)?;
let mut graph = DependencyGraph::new();
for dep in &resolved {
let package_info = self
.registry
.get_package_info(&dep.spec.name, &dep.resolved_version)?;
graph.add_package(package_info);
}
Ok(graph)
}
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: HashMap::new(),
resolved_versions: HashMap::new(),
}
}
pub fn add_package(&mut self, package_info: PackageInfo) {
let package_name = package_info.name.clone();
self.resolved_versions
.insert(package_name.clone(), package_info.version.clone());
let mut dependencies = Vec::new();
for dep in &package_info.dependencies {
dependencies.push(dep.name.clone());
}
self.edges.insert(package_name.clone(), dependencies);
self.nodes.insert(package_name, package_info);
}
pub fn topological_sort(&self) -> Result<Vec<String>> {
let mut result = Vec::new();
let mut visited = HashSet::new();
let mut in_stack = HashSet::new();
for package_name in self.nodes.keys() {
if !visited.contains(package_name) {
self.topological_sort_util(package_name, &mut visited, &mut in_stack, &mut result)?;
}
}
result.reverse();
Ok(result)
}
fn topological_sort_util(
&self,
package_name: &str,
visited: &mut HashSet<String>,
in_stack: &mut HashSet<String>,
result: &mut Vec<String>,
) -> Result<()> {
if in_stack.contains(package_name) {
return Err(TorshError::InvalidArgument(format!(
"Circular dependency detected involving package: {}",
package_name
)));
}
if visited.contains(package_name) {
return Ok(());
}
visited.insert(package_name.to_string());
in_stack.insert(package_name.to_string());
if let Some(dependencies) = self.edges.get(package_name) {
for dep in dependencies {
self.topological_sort_util(dep, visited, in_stack, result)?;
}
}
in_stack.remove(package_name);
result.push(package_name.to_string());
Ok(())
}
pub fn get_packages(&self) -> &HashMap<String, PackageInfo> {
&self.nodes
}
pub fn get_dependencies(&self, package_name: &str) -> Option<&Vec<String>> {
self.edges.get(package_name)
}
}
#[derive(Debug, Default)]
pub struct LocalPackageRegistry {
cache_dir: Option<String>,
packages: HashMap<String, Vec<PackageInfo>>,
}
impl LocalPackageRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn add_package(&mut self, package_info: PackageInfo) {
self.packages
.entry(package_info.name.clone())
.or_default()
.push(package_info);
}
}
impl PackageRegistry for LocalPackageRegistry {
fn search_packages(&self, name_pattern: &str) -> Result<Vec<PackageInfo>> {
let mut results = Vec::new();
for (name, packages) in &self.packages {
if name.contains(name_pattern) {
results.extend(packages.iter().cloned());
}
}
Ok(results)
}
fn get_versions(&self, package_name: &str) -> Result<Vec<String>> {
let versions = self
.packages
.get(package_name)
.map(|packages| packages.iter().map(|p| p.version.clone()).collect())
.unwrap_or_default();
Ok(versions)
}
fn download_package(&self, _name: &str, _version: &str, _dest_path: &Path) -> Result<()> {
Ok(())
}
fn get_package_info(&self, name: &str, version: &str) -> Result<PackageInfo> {
let packages = self
.packages
.get(name)
.ok_or_else(|| TorshError::InvalidArgument(format!("Package not found: {}", name)))?;
packages
.iter()
.find(|p| p.version == version)
.cloned()
.ok_or_else(|| {
TorshError::InvalidArgument(format!(
"Version {} not found for package: {}",
version, name
))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_package_info(name: &str, version: &str) -> PackageInfo {
PackageInfo {
name: name.to_string(),
version: version.to_string(),
description: None,
author: None,
dependencies: Vec::new(),
size: 1024,
checksum: "abc123".to_string(),
registry_url: "http://localhost".to_string(),
}
}
#[test]
fn test_dependency_spec_creation() {
let spec = DependencySpec::new("test".to_string(), "^1.0.0".to_string())
.with_feature("test-feature".to_string())
.optional()
.for_platform("linux".to_string());
assert_eq!(spec.name, "test");
assert_eq!(spec.version_req, "^1.0.0");
assert_eq!(spec.features, vec!["test-feature"]);
assert!(spec.optional);
assert_eq!(spec.platform, Some("linux".to_string()));
}
#[test]
fn test_dependency_spec_version_satisfaction() {
let spec = DependencySpec::new("test".to_string(), "^1.0.0".to_string());
assert!(spec.is_satisfied_by("1.0.0").unwrap());
assert!(spec.is_satisfied_by("1.5.0").unwrap());
assert!(!spec.is_satisfied_by("2.0.0").unwrap());
assert!(!spec.is_satisfied_by("0.9.0").unwrap());
}
#[test]
fn test_local_package_registry() {
let mut registry = LocalPackageRegistry::new();
let package_info = create_test_package_info("test-package", "1.0.0");
registry.add_package(package_info.clone());
let versions = registry.get_versions("test-package").unwrap();
assert_eq!(versions, vec!["1.0.0"]);
let retrieved_info = registry.get_package_info("test-package", "1.0.0").unwrap();
assert_eq!(retrieved_info.name, package_info.name);
assert_eq!(retrieved_info.version, package_info.version);
}
#[test]
fn test_dependency_resolution_strategy() {
let registry = Box::new(LocalPackageRegistry::new());
let resolver = DependencyResolver::new(registry)
.with_strategy(ResolutionStrategy::Highest)
.with_max_depth(50);
match resolver.strategy {
ResolutionStrategy::Highest => (),
_ => panic!("Strategy not set correctly"),
}
assert_eq!(resolver.max_depth, 50);
}
#[test]
fn test_dependency_graph() {
let mut graph = DependencyGraph::new();
let package_info = create_test_package_info("test-package", "1.0.0");
graph.add_package(package_info.clone());
assert_eq!(graph.nodes.len(), 1);
assert!(graph.nodes.contains_key("test-package"));
}
#[test]
fn test_version_selection() {
let resolver = DependencyResolver::default();
let versions = vec![
"1.0.0".to_string(),
"1.5.0".to_string(),
"2.0.0".to_string(),
];
let highest = resolver.select_highest_version(&versions).unwrap();
assert_eq!(highest, "2.0.0");
let lowest = resolver.select_lowest_version(&versions).unwrap();
assert_eq!(lowest, "1.0.0");
}
}