use crate::types::{Dependency, DependencyCheck, PackageInfo, UpdateSeverity};
use crate::version::{Version, VersionSpec};
pub struct DependencyResolver;
impl DependencyResolver {
pub fn new() -> Self {
Self
}
pub fn resolve(
&self,
dependency: &Dependency,
package_info: &PackageInfo,
installed: Option<&Version>,
) -> DependencyCheck {
let latest = package_info.latest.clone();
let in_range = self.calculate_in_range(
&dependency.version_spec,
&package_info.versions,
installed,
);
let current = installed.or_else(|| dependency.version_spec.base_version());
let (target, target_spec) = self.calculate_target(
&dependency.version_spec,
&in_range,
&latest,
current,
);
let severity = Self::calculate_severity(current, target.as_ref());
let force_spec = self.calculate_force_spec(
&dependency.version_spec,
&latest,
current,
);
DependencyCheck {
dependency: dependency.clone(),
installed: installed.cloned(),
in_range,
latest,
target,
target_spec,
severity,
force_spec,
}
}
fn calculate_target(
&self,
current_spec: &VersionSpec,
in_range: &Option<Version>,
latest: &Version,
current: Option<&Version>,
) -> (Option<Version>, Option<VersionSpec>) {
let current = match current {
Some(c) => c,
None => return (Some(latest.clone()), None),
};
if let Some(ir) = in_range
&& ir > current {
let spec = if current_spec.is_rewritable() {
Some(current_spec.with_version(ir))
} else {
None
};
return (Some(ir.clone()), spec);
}
if latest > current {
let spec = if current_spec.is_rewritable() {
Some(current_spec.with_version(latest))
} else {
None
};
return (Some(latest.clone()), spec);
}
(None, None)
}
fn calculate_force_spec(
&self,
current_spec: &VersionSpec,
latest: &Version,
current: Option<&Version>,
) -> Option<VersionSpec> {
let current = current?;
if latest > current && current_spec.is_rewritable() {
Some(current_spec.with_version(latest))
} else {
None
}
}
pub fn calculate_severity(
current: Option<&Version>,
target: Option<&Version>,
) -> Option<UpdateSeverity> {
let current = current?;
let target = target?;
if target.major > current.major {
Some(UpdateSeverity::Major)
} else if target.minor > current.minor {
Some(UpdateSeverity::Minor)
} else if target.patch > current.patch {
Some(UpdateSeverity::Patch)
} else {
None
}
}
fn calculate_in_range(
&self,
spec: &VersionSpec,
available_versions: &[Version],
installed: Option<&Version>,
) -> Option<Version> {
available_versions
.iter()
.filter(|v| {
if !spec.satisfies(v) {
return false;
}
match spec {
VersionSpec::Minimum(base) | VersionSpec::GreaterThan(base) => {
let target_major = if let Some(inst) = installed {
base.major.max(inst.major)
} else {
base.major
};
v.major == target_major
}
_ => true,
}
})
.max()
.cloned()
}
}
impl Default for DependencyResolver {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::str::FromStr;
fn create_test_dependency(name: &str, spec_str: &str) -> Dependency {
Dependency {
name: name.to_string(),
version_spec: VersionSpec::parse(spec_str).unwrap(),
source_file: PathBuf::from("test.txt"),
line_number: 1,
original_line: format!("{}=={}", name, spec_str),
}
}
fn create_package_info(name: &str, versions: Vec<&str>) -> PackageInfo {
let version_objects: Vec<Version> = versions
.iter()
.map(|v| Version::from_str(v).unwrap())
.collect();
let latest = version_objects.last().unwrap().clone();
PackageInfo {
name: name.to_string(),
versions: version_objects,
latest: latest.clone(),
latest_stable: Some(latest),
}
}
#[test]
fn test_in_range_update() {
let resolver = DependencyResolver::new();
let dep = create_test_dependency("requests", ">=2.28.0,<3.0.0");
let pkg_info = create_package_info("requests", vec!["2.28.0", "2.32.3", "3.1.0"]);
let installed = Version::from_str("2.28.0").unwrap();
let result = resolver.resolve(&dep, &pkg_info, Some(&installed));
assert!(result.target.is_some());
assert_eq!(result.target.as_ref().unwrap().to_string(), "2.32.3");
assert_eq!(result.severity, Some(UpdateSeverity::Minor));
assert!(result.has_newer_available());
}
#[test]
fn test_force_only_update() {
let resolver = DependencyResolver::new();
let dep = create_test_dependency("flask", "^2.0.0");
let pkg_info = create_package_info("flask", vec!["2.0.0", "2.3.3", "3.0.0"]);
let installed = Version::from_str("2.3.3").unwrap();
let result = resolver.resolve(&dep, &pkg_info, Some(&installed));
assert!(result.target.is_some());
assert_eq!(result.target.as_ref().unwrap().to_string(), "3.0.0");
assert_eq!(result.severity, Some(UpdateSeverity::Major));
assert!(!result.has_newer_available());
}
#[test]
fn test_no_update_needed() {
let resolver = DependencyResolver::new();
let dep = create_test_dependency("flask", ">=2.3.3");
let pkg_info = create_package_info("flask", vec!["2.0.0", "2.3.3"]);
let installed = Version::from_str("2.3.3").unwrap();
let result = resolver.resolve(&dep, &pkg_info, Some(&installed));
assert!(result.target.is_none());
assert!(!result.has_update());
}
}