vx_version/
utils.rs

1//! Version utility functions
2
3use crate::VersionInfo;
4
5/// Version comparison and manipulation utilities
6pub struct VersionUtils;
7
8impl VersionUtils {
9    /// Sort versions in descending order (latest first)
10    pub fn sort_versions_desc(mut versions: Vec<VersionInfo>) -> Vec<VersionInfo> {
11        versions.sort_by(|a, b| {
12            // Use semantic version comparison for better sorting
13            match (
14                Self::parse_semantic_version(&a.version),
15                Self::parse_semantic_version(&b.version),
16            ) {
17                (Ok(va), Ok(vb)) => vb.cmp(&va), // Descending order
18                _ => b.version.cmp(&a.version),  // Fallback to string comparison
19            }
20        });
21        versions
22    }
23
24    /// Filter out prerelease versions
25    pub fn filter_stable_only(versions: Vec<VersionInfo>) -> Vec<VersionInfo> {
26        versions.into_iter().filter(|v| !v.prerelease).collect()
27    }
28
29    /// Get the latest N versions
30    pub fn take_latest(versions: Vec<VersionInfo>, count: usize) -> Vec<VersionInfo> {
31        versions.into_iter().take(count).collect()
32    }
33
34    /// Filter versions by LTS status
35    pub fn filter_lts_only(versions: Vec<VersionInfo>) -> Vec<VersionInfo> {
36        versions
37            .into_iter()
38            .filter(|v| {
39                v.metadata
40                    .get("lts")
41                    .map(|val| val == "true")
42                    .unwrap_or(false)
43            })
44            .collect()
45    }
46
47    /// Find a specific version in a list
48    pub fn find_version<'a>(versions: &'a [VersionInfo], version: &str) -> Option<&'a VersionInfo> {
49        versions.iter().find(|v| v.version == version)
50    }
51
52    /// Check if a version string matches a pattern
53    pub fn matches_pattern(version: &str, pattern: &str) -> bool {
54        match pattern {
55            "latest" => true,
56            "stable" => !Self::is_prerelease(version),
57            "lts" => false, // Would need additional metadata
58            _ => version == pattern,
59        }
60    }
61
62    /// Check if a version string indicates a prerelease
63    pub fn is_prerelease(version: &str) -> bool {
64        version.contains("alpha")
65            || version.contains("beta")
66            || version.contains("rc")
67            || version.contains("pre")
68            || version.contains("dev")
69            || version.contains("snapshot")
70    }
71
72    /// Clean version string (remove prefixes like 'v', 'go', etc.)
73    pub fn clean_version(version: &str, prefixes: &[&str]) -> String {
74        let mut cleaned = version.to_string();
75        for prefix in prefixes {
76            if cleaned.starts_with(prefix) {
77                cleaned = cleaned[prefix.len()..].to_string();
78                break;
79            }
80        }
81        cleaned
82    }
83
84    /// Parse a semantic version string into comparable components
85    fn parse_semantic_version(version: &str) -> Result<(u32, u32, u32, String), String> {
86        let clean_version = version.trim_start_matches('v');
87        let parts: Vec<&str> = clean_version.split('.').collect();
88
89        if parts.len() < 2 {
90            return Err(format!("Invalid version format: {}", version));
91        }
92
93        let major = parts[0]
94            .parse::<u32>()
95            .map_err(|_| format!("Invalid major version: {}", parts[0]))?;
96
97        let minor = parts[1]
98            .parse::<u32>()
99            .map_err(|_| format!("Invalid minor version: {}", parts[1]))?;
100
101        let (patch, suffix) = if parts.len() > 2 {
102            let patch_part = parts[2];
103            if let Some(dash_pos) = patch_part.find('-') {
104                let patch_num = patch_part[..dash_pos]
105                    .parse::<u32>()
106                    .map_err(|_| format!("Invalid patch version: {}", &patch_part[..dash_pos]))?;
107                let suffix = patch_part[dash_pos..].to_string();
108                (patch_num, suffix)
109            } else {
110                let patch_num = patch_part
111                    .parse::<u32>()
112                    .map_err(|_| format!("Invalid patch version: {}", patch_part))?;
113                (patch_num, String::new())
114            }
115        } else {
116            (0, String::new())
117        };
118
119        Ok((major, minor, patch, suffix))
120    }
121
122    /// Compare two version strings
123    pub fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
124        match (
125            Self::parse_semantic_version(a),
126            Self::parse_semantic_version(b),
127        ) {
128            (Ok(va), Ok(vb)) => va.cmp(&vb),
129            _ => a.cmp(b), // Fallback to string comparison
130        }
131    }
132
133    /// Check if version A is greater than version B
134    pub fn is_greater_than(a: &str, b: &str) -> bool {
135        Self::compare_versions(a, b) == std::cmp::Ordering::Greater
136    }
137
138    /// Check if version A is less than version B
139    pub fn is_less_than(a: &str, b: &str) -> bool {
140        Self::compare_versions(a, b) == std::cmp::Ordering::Less
141    }
142
143    /// Check if two versions are equal
144    pub fn is_equal(a: &str, b: &str) -> bool {
145        Self::compare_versions(a, b) == std::cmp::Ordering::Equal
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_version_utils() {
155        assert!(VersionUtils::is_prerelease("1.0.0-alpha"));
156        assert!(VersionUtils::is_prerelease("2.0.0-beta.1"));
157        assert!(VersionUtils::is_prerelease("3.0.0-rc.1"));
158        assert!(!VersionUtils::is_prerelease("1.0.0"));
159
160        assert_eq!(VersionUtils::clean_version("v1.0.0", &["v"]), "1.0.0");
161        assert_eq!(VersionUtils::clean_version("go1.21.0", &["go"]), "1.21.0");
162        assert_eq!(VersionUtils::clean_version("1.0.0", &["v", "go"]), "1.0.0");
163    }
164
165    #[test]
166    fn test_version_comparison() {
167        assert!(VersionUtils::is_greater_than("1.2.3", "1.2.2"));
168        assert!(VersionUtils::is_less_than("1.2.2", "1.2.3"));
169        assert!(VersionUtils::is_equal("1.2.3", "1.2.3"));
170
171        assert!(VersionUtils::is_greater_than("2.0.0", "1.9.9"));
172        assert!(VersionUtils::is_greater_than("1.3.0", "1.2.9"));
173    }
174
175    #[test]
176    fn test_matches_pattern() {
177        assert!(VersionUtils::matches_pattern("1.2.3", "latest"));
178        assert!(VersionUtils::matches_pattern("1.2.3", "stable"));
179        assert!(!VersionUtils::matches_pattern("1.2.3-alpha", "stable"));
180        assert!(VersionUtils::matches_pattern("1.2.3", "1.2.3"));
181        assert!(!VersionUtils::matches_pattern("1.2.3", "1.2.4"));
182    }
183
184    #[test]
185    fn test_filter_stable_only() {
186        let versions = vec![
187            VersionInfo::new("1.0.0".to_string()),
188            VersionInfo::new("1.1.0-alpha".to_string()).as_prerelease(),
189            VersionInfo::new("1.1.0".to_string()),
190        ];
191
192        let stable = VersionUtils::filter_stable_only(versions);
193        assert_eq!(stable.len(), 2);
194        assert_eq!(stable[0].version, "1.0.0");
195        assert_eq!(stable[1].version, "1.1.0");
196    }
197}