1use crate::types::{Dependency, DependencyCheck, PackageInfo, UpdateSeverity};
2use crate::version::{Version, VersionSpec};
3
4pub struct DependencyResolver;
6
7impl DependencyResolver {
8 pub fn new() -> Self {
9 Self
10 }
11
12 pub fn resolve(
14 &self,
15 dependency: &Dependency,
16 package_info: &PackageInfo,
17 installed: Option<&Version>,
18 ) -> DependencyCheck {
19 let latest = package_info.latest.clone();
20
21 let in_range = self.calculate_in_range(
23 &dependency.version_spec,
24 &package_info.versions,
25 installed,
26 );
27
28 let current = installed.or_else(|| dependency.version_spec.base_version());
30
31 let (target, target_spec) = self.calculate_target(
32 &dependency.version_spec,
33 &in_range,
34 &latest,
35 current,
36 );
37
38 let severity = Self::calculate_severity(current, target.as_ref());
40
41 let force_spec = self.calculate_force_spec(
43 &dependency.version_spec,
44 &latest,
45 current,
46 );
47
48 DependencyCheck {
49 dependency: dependency.clone(),
50 installed: installed.cloned(),
51 in_range,
52 latest,
53 target,
54 target_spec,
55 severity,
56 force_spec,
57 }
58 }
59
60 fn calculate_target(
62 &self,
63 current_spec: &VersionSpec,
64 in_range: &Option<Version>,
65 latest: &Version,
66 current: Option<&Version>,
67 ) -> (Option<Version>, Option<VersionSpec>) {
68 let current = match current {
69 Some(c) => c,
70 None => return (Some(latest.clone()), None),
74 };
75
76 if let Some(ir) = in_range
78 && ir > current {
79 let spec = if current_spec.is_rewritable() {
80 Some(current_spec.with_version(ir))
81 } else {
82 None
83 };
84 return (Some(ir.clone()), spec);
85 }
86
87 if latest > current {
89 let spec = if current_spec.is_rewritable() {
90 Some(current_spec.with_version(latest))
91 } else {
92 None
93 };
94 return (Some(latest.clone()), spec);
95 }
96
97 (None, None)
98 }
99
100 fn calculate_force_spec(
102 &self,
103 current_spec: &VersionSpec,
104 latest: &Version,
105 current: Option<&Version>,
106 ) -> Option<VersionSpec> {
107 let current = current?;
108
109 if latest > current && current_spec.is_rewritable() {
110 Some(current_spec.with_version(latest))
111 } else {
112 None
113 }
114 }
115
116 pub fn calculate_severity(
118 current: Option<&Version>,
119 target: Option<&Version>,
120 ) -> Option<UpdateSeverity> {
121 let current = current?;
122 let target = target?;
123
124 if target.major > current.major {
125 Some(UpdateSeverity::Major)
126 } else if target.minor > current.minor {
127 Some(UpdateSeverity::Minor)
128 } else if target.patch > current.patch {
129 Some(UpdateSeverity::Patch)
130 } else {
131 None
132 }
133 }
134
135 fn calculate_in_range(
137 &self,
138 spec: &VersionSpec,
139 available_versions: &[Version],
140 installed: Option<&Version>,
141 ) -> Option<Version> {
142 available_versions
143 .iter()
144 .filter(|v| {
145 if !spec.satisfies(v) {
147 return false;
148 }
149
150 match spec {
152 VersionSpec::Minimum(base) | VersionSpec::GreaterThan(base) => {
153 let target_major = if let Some(inst) = installed {
154 base.major.max(inst.major)
155 } else {
156 base.major
157 };
158 v.major == target_major
159 }
160 _ => true,
161 }
162 })
163 .max()
164 .cloned()
165 }
166}
167
168impl Default for DependencyResolver {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use std::path::PathBuf;
178 use std::str::FromStr;
179
180 fn create_test_dependency(name: &str, spec_str: &str) -> Dependency {
181 Dependency {
182 name: name.to_string(),
183 version_spec: VersionSpec::parse(spec_str).unwrap(),
184 source_file: PathBuf::from("test.txt"),
185 line_number: 1,
186 original_line: format!("{}=={}", name, spec_str),
187 }
188 }
189
190 fn create_package_info(name: &str, versions: Vec<&str>) -> PackageInfo {
191 let version_objects: Vec<Version> = versions
192 .iter()
193 .map(|v| Version::from_str(v).unwrap())
194 .collect();
195 let latest = version_objects.last().unwrap().clone();
196
197 PackageInfo {
198 name: name.to_string(),
199 versions: version_objects,
200 latest: latest.clone(),
201 latest_stable: Some(latest),
202 }
203 }
204
205 #[test]
206 fn test_in_range_update() {
207 let resolver = DependencyResolver::new();
208 let dep = create_test_dependency("requests", ">=2.28.0,<3.0.0");
209 let pkg_info = create_package_info("requests", vec!["2.28.0", "2.32.3", "3.1.0"]);
210
211 let installed = Version::from_str("2.28.0").unwrap();
212 let result = resolver.resolve(&dep, &pkg_info, Some(&installed));
213
214 assert!(result.target.is_some());
216 assert_eq!(result.target.as_ref().unwrap().to_string(), "2.32.3");
217 assert_eq!(result.severity, Some(UpdateSeverity::Minor));
218
219 assert!(result.has_newer_available());
221 }
222
223 #[test]
224 fn test_force_only_update() {
225 let resolver = DependencyResolver::new();
226 let dep = create_test_dependency("flask", "^2.0.0");
227 let pkg_info = create_package_info("flask", vec!["2.0.0", "2.3.3", "3.0.0"]);
228
229 let installed = Version::from_str("2.3.3").unwrap();
231 let result = resolver.resolve(&dep, &pkg_info, Some(&installed));
232
233 assert!(result.target.is_some());
235 assert_eq!(result.target.as_ref().unwrap().to_string(), "3.0.0");
236 assert_eq!(result.severity, Some(UpdateSeverity::Major));
237
238 assert!(!result.has_newer_available());
240 }
241
242 #[test]
243 fn test_no_update_needed() {
244 let resolver = DependencyResolver::new();
245 let dep = create_test_dependency("flask", ">=2.3.3");
246 let pkg_info = create_package_info("flask", vec!["2.0.0", "2.3.3"]);
247
248 let installed = Version::from_str("2.3.3").unwrap();
249 let result = resolver.resolve(&dep, &pkg_info, Some(&installed));
250
251 assert!(result.target.is_none());
253 assert!(!result.has_update());
254 }
255}