image_optimizer/updater/
version_comparator.rs

1use anyhow::Result;
2
3/// Compares two semantic version strings to determine if an update is available.
4///
5/// This function implements semantic version comparison following the semver specification.
6/// It automatically strips 'v' prefixes and compares major.minor.patch version numbers.
7/// The comparison determines whether the latest version is newer than the current version.
8///
9/// # Arguments
10///
11/// * `current` - The current version string (e.g., "1.2.1" or "v1.2.1")
12/// * `latest` - The latest available version string (e.g., "1.2.2" or "v1.2.2")
13///
14/// # Returns
15///
16/// Returns `true` if the latest version is newer than the current version,
17/// `false` if the current version is newer or equal to the latest version.
18///
19/// # Errors
20///
21/// Returns an error if either version string cannot be parsed as a valid
22/// semantic version (must contain numeric parts separated by dots).
23///
24/// # Examples
25///
26/// ```rust
27/// use image_optimizer::updater::version_comparator::compare_versions;
28///
29/// # fn example() -> anyhow::Result<()> {
30/// // Update available
31/// assert!(compare_versions("1.0.0", "1.0.1")?);
32/// assert!(compare_versions("v1.0.0", "v1.1.0")?);
33///
34/// // No update needed
35/// assert!(!compare_versions("1.0.1", "1.0.0")?);
36/// assert!(!compare_versions("1.0.0", "1.0.0")?);
37/// # Ok(())
38/// # }
39/// ```
40pub fn compare_versions(current: &str, latest: &str) -> Result<bool> {
41    let current_clean = current.trim_start_matches('v');
42    let latest_clean = latest.trim_start_matches('v');
43
44    let parse_version = |v: &str| -> Result<Vec<u32>> {
45        v.split('.')
46            .map(|part| {
47                part.parse::<u32>()
48                    .map_err(|e| anyhow::anyhow!("Invalid version format: {}", e))
49            })
50            .collect()
51    };
52
53    let current_parts = parse_version(current_clean)?;
54    let latest_parts = parse_version(latest_clean)?;
55
56    // Compare version parts (major.minor.patch)
57    for (curr, latest) in current_parts.iter().zip(latest_parts.iter()) {
58        if latest > curr {
59            return Ok(true); // Update available
60        } else if curr > latest {
61            return Ok(false); // Current is newer
62        }
63    }
64
65    // If all compared parts are equal, check if latest has more parts
66    Ok(latest_parts.len() > current_parts.len())
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn test_version_comparison() {
75        assert!(compare_versions("1.0.0", "1.0.1").unwrap());
76        assert!(compare_versions("1.0.0", "1.1.0").unwrap());
77        assert!(compare_versions("1.0.0", "2.0.0").unwrap());
78        assert!(!compare_versions("1.0.1", "1.0.0").unwrap());
79        assert!(!compare_versions("1.1.0", "1.0.0").unwrap());
80        assert!(!compare_versions("2.0.0", "1.0.0").unwrap());
81        assert!(!compare_versions("1.0.0", "1.0.0").unwrap());
82    }
83
84    #[test]
85    fn test_version_with_v_prefix() {
86        assert!(compare_versions("v1.0.0", "v1.0.1").unwrap());
87        assert!(compare_versions("1.0.0", "v1.0.1").unwrap());
88        assert!(compare_versions("v1.0.0", "1.0.1").unwrap());
89    }
90
91    #[test]
92    fn test_different_version_lengths() {
93        assert!(compare_versions("1.0", "1.0.1").unwrap());
94        assert!(!compare_versions("1.0.1", "1.0").unwrap());
95        assert!(!compare_versions("1.0", "1.0").unwrap());
96    }
97
98    #[test]
99    fn test_invalid_version_format() {
100        assert!(compare_versions("invalid", "1.0.0").is_err());
101        assert!(compare_versions("1.0.0", "invalid").is_err());
102        assert!(compare_versions("1.x.0", "1.0.0").is_err());
103    }
104
105    #[test]
106    fn test_real_world_versions() {
107        assert!(compare_versions("1.2.1", "1.2.2").unwrap());
108        assert!(compare_versions("0.9.0", "1.0.0").unwrap());
109        assert!(!compare_versions("2.0.0", "1.9.9").unwrap());
110    }
111}