agpm_cli/version/comparison.rs
1//! Version comparison utilities for semantic version handling.
2//!
3//! This module provides utilities for comparing semantic versions, checking for
4//! newer versions, and handling version parsing operations used throughout the
5//! dependency resolution process. It supports common version prefixes and
6//! provides error-handling for malformed version strings.
7//!
8//! # Features
9//!
10//! - **Semantic Version Parsing**: Handles `v1.2.3`, `version-1.2.3`, `release-1.2.3` formats
11//! - **Version Comparison**: Find newer versions and latest versions in collections
12//! - **Prefix Handling**: Automatically strips common version prefixes
13//! - **Error Resilience**: Gracefully handles malformed version strings
14//!
15//! # Examples
16//!
17//! ```rust,no_run
18//! use agpm_cli::version::comparison::VersionComparator;
19//!
20//! # fn example() -> anyhow::Result<()> {
21//! let versions = vec![
22//! "v1.0.0".to_string(),
23//! "v1.1.0".to_string(),
24//! "v2.0.0".to_string(),
25//! "version-1.0.1".to_string(),
26//! ];
27//!
28//! // Check if there are newer versions
29//! let has_newer = VersionComparator::has_newer_version("v1.0.0", &versions)?;
30//! assert!(has_newer);
31//!
32//! // Get the latest version
33//! let latest = VersionComparator::get_latest(&versions)?
34//! .expect("Should find latest version");
35//! assert_eq!(latest, "v2.0.0");
36//!
37//! // Get all newer versions than current
38//! let newer = VersionComparator::get_newer_versions("v1.0.0", &versions)?;
39//! assert_eq!(newer.len(), 3); // v1.1.0, v2.0.0, version-1.0.1
40//! # Ok(())
41//! # }
42//! ```
43
44use anyhow::Result;
45use semver::Version;
46
47/// Version comparison utilities for semantic version operations.
48///
49/// This struct provides static methods for comparing semantic versions,
50/// finding newer versions, and handling version parsing with common prefixes.
51/// All methods are designed to handle malformed version strings gracefully.
52pub struct VersionComparator;
53
54impl VersionComparator {
55 /// Checks if there are newer versions available than the current version.
56 ///
57 /// This method compares the current version against a list of available versions
58 /// and returns `true` if any version is semantically newer.
59 ///
60 /// # Arguments
61 ///
62 /// * `current` - The current version string (e.g., "v1.0.0", "1.2.3")
63 /// * `versions` - A slice of version strings to compare against
64 ///
65 /// # Returns
66 ///
67 /// `Ok(true)` if newer versions exist, `Ok(false)` if current is latest.
68 ///
69 /// # Errors
70 ///
71 /// Returns an error if the current version string cannot be parsed as a
72 /// semantic version. Malformed versions in the comparison list are ignored.
73 ///
74 /// # Examples
75 ///
76 /// ```rust,no_run
77 /// use agpm_cli::version::comparison::VersionComparator;
78 ///
79 /// # fn example() -> anyhow::Result<()> {
80 /// let versions = vec!["v1.0.0".to_string(), "v1.1.0".to_string(), "v2.0.0".to_string()];
81 ///
82 /// // Check if v1.0.0 has newer versions available
83 /// let has_newer = VersionComparator::has_newer_version("v1.0.0", &versions)?;
84 /// assert!(has_newer);
85 ///
86 /// // Check if v2.0.0 is the latest
87 /// let has_newer = VersionComparator::has_newer_version("v2.0.0", &versions)?;
88 /// assert!(!has_newer);
89 /// # Ok(())
90 /// # }
91 /// ```
92 pub fn has_newer_version(current: &str, versions: &[String]) -> Result<bool> {
93 let current_version = Self::parse_version(current)?;
94
95 for version_str in versions {
96 if let Ok(version) = Self::parse_version(version_str)
97 && version > current_version
98 {
99 return Ok(true);
100 }
101 }
102
103 Ok(false)
104 }
105
106 /// Gets all versions newer than the current version, sorted by version descending.
107 ///
108 /// This method finds all versions in the provided list that are semantically
109 /// newer than the current version and returns them sorted from newest to oldest.
110 ///
111 /// # Arguments
112 ///
113 /// * `current` - The current version string to compare against
114 /// * `versions` - A slice of version strings to search
115 ///
116 /// # Returns
117 ///
118 /// A vector of references to version strings that are newer than current,
119 /// sorted in descending order (newest first).
120 ///
121 /// # Errors
122 ///
123 /// Returns an error if the current version string cannot be parsed.
124 /// Malformed versions in the search list are silently ignored.
125 ///
126 /// # Examples
127 ///
128 /// ```rust,no_run
129 /// use agpm_cli::version::comparison::VersionComparator;
130 ///
131 /// # fn example() -> anyhow::Result<()> {
132 /// let versions = vec![
133 /// "v1.0.0".to_string(),
134 /// "v1.2.0".to_string(),
135 /// "v1.1.0".to_string(),
136 /// "v2.0.0".to_string(),
137 /// ];
138 ///
139 /// let newer = VersionComparator::get_newer_versions("v1.0.0", &versions)?;
140 /// assert_eq!(newer.len(), 3);
141 /// // Results are sorted newest first
142 /// assert_eq!(newer[0], "v2.0.0");
143 /// assert_eq!(newer[1], "v1.2.0");
144 /// assert_eq!(newer[2], "v1.1.0");
145 /// # Ok(())
146 /// # }
147 /// ```
148 pub fn get_newer_versions<'a>(
149 current: &str,
150 versions: &'a [String],
151 ) -> Result<Vec<&'a String>> {
152 let current_version = Self::parse_version(current)?;
153 let mut newer = Vec::new();
154
155 for version_str in versions {
156 if let Ok(version) = Self::parse_version(version_str)
157 && version > current_version
158 {
159 newer.push(version_str);
160 }
161 }
162
163 // Sort by version descending
164 newer.sort_by(|a, b| {
165 let v1 = Self::parse_version(a).unwrap_or_else(|_| Version::new(0, 0, 0));
166 let v2 = Self::parse_version(b).unwrap_or_else(|_| Version::new(0, 0, 0));
167 v2.cmp(&v1)
168 });
169
170 Ok(newer)
171 }
172
173 /// Gets the latest (highest) semantic version from a list of versions.
174 ///
175 /// This method finds the semantically highest version from the provided list,
176 /// ignoring any malformed version strings.
177 ///
178 /// # Arguments
179 ///
180 /// * `versions` - A slice of version strings to search
181 ///
182 /// # Returns
183 ///
184 /// `Ok(Some(&String))` with the latest version, or `Ok(None)` if the list is
185 /// empty or contains no valid semantic versions.
186 ///
187 /// # Errors
188 ///
189 /// This method does not return errors - malformed version strings are silently
190 /// ignored during comparison.
191 ///
192 /// # Examples
193 ///
194 /// ```rust,no_run
195 /// use agpm_cli::version::comparison::VersionComparator;
196 ///
197 /// # fn example() -> anyhow::Result<()> {
198 /// let versions = vec![
199 /// "v1.0.0".to_string(),
200 /// "v2.0.0".to_string(),
201 /// "v1.5.0".to_string(),
202 /// "invalid-version".to_string(), // This will be ignored
203 /// ];
204 ///
205 /// let latest = VersionComparator::get_latest(&versions)?
206 /// .expect("Should find a latest version");
207 /// assert_eq!(latest, "v2.0.0");
208 ///
209 /// // Empty list returns None
210 /// let empty: Vec<String> = vec![];
211 /// assert!(VersionComparator::get_latest(&empty)?.is_none());
212 /// # Ok(())
213 /// # }
214 /// ```
215 pub fn get_latest(versions: &[String]) -> Result<Option<&String>> {
216 if versions.is_empty() {
217 return Ok(None);
218 }
219
220 let mut latest: Option<(&String, Version)> = None;
221
222 for version_str in versions {
223 if let Ok(version) = Self::parse_version(version_str)
224 && (latest.is_none() || version > latest.as_ref().unwrap().1)
225 {
226 latest = Some((version_str, version));
227 }
228 }
229
230 Ok(latest.map(|(s, _)| s))
231 }
232
233 /// Parses a version string, automatically handling common prefixes.
234 ///
235 /// This private method normalizes version strings by removing common prefixes
236 /// like "v", "version-", and "release-" before parsing with the semver crate.
237 ///
238 /// # Supported Prefixes
239 ///
240 /// - `v1.2.3` → `1.2.3`
241 /// - `version-1.2.3` → `1.2.3`
242 /// - `release-1.2.3` → `1.2.3`
243 /// - `1.2.3` → `1.2.3` (no change)
244 ///
245 /// # Arguments
246 ///
247 /// * `version_str` - The version string to parse
248 ///
249 /// # Returns
250 ///
251 /// A parsed `semver::Version` instance.
252 ///
253 /// # Errors
254 ///
255 /// Returns an error if the version string (after prefix removal) is not
256 /// a valid semantic version according to the semver specification.
257 fn parse_version(version_str: &str) -> Result<Version> {
258 // Remove common version prefixes
259 let clean_version = if let Some(stripped) = version_str.strip_prefix("version-") {
260 stripped
261 } else if let Some(stripped) = version_str.strip_prefix("release-") {
262 stripped
263 } else if let Some(stripped) = version_str.strip_prefix('v') {
264 stripped
265 } else {
266 version_str
267 };
268
269 Ok(Version::parse(clean_version)?)
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_has_newer_version() {
279 let versions = vec!["v1.0.0".to_string(), "v1.1.0".to_string(), "v2.0.0".to_string()];
280
281 assert!(VersionComparator::has_newer_version("1.0.0", &versions).unwrap());
282 assert!(VersionComparator::has_newer_version("v1.0.0", &versions).unwrap());
283 assert!(!VersionComparator::has_newer_version("2.0.0", &versions).unwrap());
284 assert!(!VersionComparator::has_newer_version("v3.0.0", &versions).unwrap());
285 }
286
287 #[test]
288 fn test_get_newer_versions() {
289 let versions = vec![
290 "v1.0.0".to_string(),
291 "v1.1.0".to_string(),
292 "v2.0.0".to_string(),
293 "v0.9.0".to_string(),
294 ];
295
296 let newer = VersionComparator::get_newer_versions("1.0.0", &versions).unwrap();
297 assert_eq!(newer.len(), 2);
298 assert_eq!(newer[0], "v2.0.0");
299 assert_eq!(newer[1], "v1.1.0");
300
301 let newer = VersionComparator::get_newer_versions("2.0.0", &versions).unwrap();
302 assert_eq!(newer.len(), 0);
303 }
304
305 #[test]
306 fn test_get_latest() {
307 let versions = vec![
308 "v1.0.0".to_string(),
309 "v1.1.0".to_string(),
310 "v2.0.0".to_string(),
311 "v0.9.0".to_string(),
312 ];
313
314 let latest = VersionComparator::get_latest(&versions).unwrap();
315 assert_eq!(latest, Some(&"v2.0.0".to_string()));
316
317 let empty: Vec<String> = vec![];
318 let latest = VersionComparator::get_latest(&empty).unwrap();
319 assert_eq!(latest, None);
320 }
321
322 #[test]
323 fn test_parse_version() {
324 assert_eq!(VersionComparator::parse_version("1.0.0").unwrap(), Version::new(1, 0, 0));
325 assert_eq!(VersionComparator::parse_version("v1.0.0").unwrap(), Version::new(1, 0, 0));
326 assert_eq!(
327 VersionComparator::parse_version("version-1.0.0").unwrap(),
328 Version::new(1, 0, 0)
329 );
330 assert_eq!(
331 VersionComparator::parse_version("release-1.0.0").unwrap(),
332 Version::new(1, 0, 0)
333 );
334 }
335}