vx_dependency/
version.rs

1//! Version handling and semantic version matching
2
3use crate::{Error, Result};
4use serde::{Deserialize, Serialize};
5use std::cmp::Ordering;
6use std::fmt;
7
8/// Semantic version representation
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct Version {
11    /// Major version
12    pub major: u64,
13    /// Minor version
14    pub minor: u64,
15    /// Patch version
16    pub patch: u64,
17    /// Pre-release identifier
18    pub prerelease: Option<String>,
19    /// Build metadata
20    pub build: Option<String>,
21}
22
23/// Version range specification
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct VersionRange {
26    /// Range expression (e.g., ">=1.0.0", "^2.1.0", "~1.2.3")
27    pub expression: String,
28    /// Parsed range components
29    pub components: Vec<VersionRangeComponent>,
30}
31
32/// Component of a version range
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct VersionRangeComponent {
35    /// Operator (>=, >, <=, <, =, ^, ~)
36    pub operator: VersionOperator,
37    /// Target version
38    pub version: Version,
39}
40
41/// Version comparison operators
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub enum VersionOperator {
44    /// Exact match (=)
45    Equal,
46    /// Greater than (>)
47    GreaterThan,
48    /// Greater than or equal (>=)
49    GreaterThanOrEqual,
50    /// Less than (<)
51    LessThan,
52    /// Less than or equal (<=)
53    LessThanOrEqual,
54    /// Compatible release (^) - allows patch and minor updates
55    Caret,
56    /// Tilde range (~) - allows patch updates only
57    Tilde,
58}
59
60/// Version matcher for checking constraints
61pub struct VersionMatcher;
62
63impl Version {
64    /// Parse a version string
65    pub fn parse(version_str: &str) -> Result<Self> {
66        let version_str = version_str.trim();
67
68        // Handle 'v' prefix
69        let version_str = version_str.strip_prefix('v').unwrap_or(version_str);
70
71        // Split by '+' to separate build metadata
72        let (version_part, build) = if let Some(pos) = version_str.find('+') {
73            let (v, b) = version_str.split_at(pos);
74            (v, Some(b[1..].to_string()))
75        } else {
76            (version_str, None)
77        };
78
79        // Split by '-' to separate prerelease
80        let (core_version, prerelease) = if let Some(pos) = version_part.find('-') {
81            let (v, p) = version_part.split_at(pos);
82            (v, Some(p[1..].to_string()))
83        } else {
84            (version_part, None)
85        };
86
87        // Parse major.minor.patch
88        let parts: Vec<&str> = core_version.split('.').collect();
89        if parts.len() < 2 {
90            return Err(Error::InvalidVersionConstraint {
91                constraint: version_str.to_string(),
92            });
93        }
94
95        let major = parts[0]
96            .parse()
97            .map_err(|_| Error::InvalidVersionConstraint {
98                constraint: version_str.to_string(),
99            })?;
100
101        let minor = parts[1]
102            .parse()
103            .map_err(|_| Error::InvalidVersionConstraint {
104                constraint: version_str.to_string(),
105            })?;
106
107        let patch = if parts.len() > 2 {
108            parts[2]
109                .parse()
110                .map_err(|_| Error::InvalidVersionConstraint {
111                    constraint: version_str.to_string(),
112                })?
113        } else {
114            0
115        };
116
117        Ok(Version {
118            major,
119            minor,
120            patch,
121            prerelease,
122            build,
123        })
124    }
125
126    /// Check if this is a prerelease version
127    pub fn is_prerelease(&self) -> bool {
128        self.prerelease.is_some()
129    }
130
131    /// Get the core version without prerelease/build metadata
132    pub fn core_version(&self) -> String {
133        format!("{}.{}.{}", self.major, self.minor, self.patch)
134    }
135}
136
137impl fmt::Display for Version {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
140
141        if let Some(ref prerelease) = self.prerelease {
142            write!(f, "-{}", prerelease)?;
143        }
144
145        if let Some(ref build) = self.build {
146            write!(f, "+{}", build)?;
147        }
148
149        Ok(())
150    }
151}
152
153impl PartialOrd for Version {
154    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155        Some(self.cmp(other))
156    }
157}
158
159impl Ord for Version {
160    fn cmp(&self, other: &Self) -> Ordering {
161        // Compare major.minor.patch
162        match self.major.cmp(&other.major) {
163            Ordering::Equal => {}
164            other => return other,
165        }
166
167        match self.minor.cmp(&other.minor) {
168            Ordering::Equal => {}
169            other => return other,
170        }
171
172        match self.patch.cmp(&other.patch) {
173            Ordering::Equal => {}
174            other => return other,
175        }
176
177        // Handle prerelease comparison
178        match (&self.prerelease, &other.prerelease) {
179            (None, None) => Ordering::Equal,
180            (Some(_), None) => Ordering::Less, // Prerelease < release
181            (None, Some(_)) => Ordering::Greater, // Release > prerelease
182            (Some(a), Some(b)) => a.cmp(b),    // Compare prerelease strings
183        }
184    }
185}
186
187impl VersionRange {
188    /// Parse a version range expression
189    pub fn parse(expression: &str) -> Result<Self> {
190        let expression = expression.trim();
191        let components = Self::parse_components(expression)?;
192
193        Ok(VersionRange {
194            expression: expression.to_string(),
195            components,
196        })
197    }
198
199    /// Check if a version satisfies this range
200    pub fn satisfies(&self, version: &Version) -> bool {
201        self.components
202            .iter()
203            .all(|component| component.satisfies(version))
204    }
205
206    fn parse_components(expression: &str) -> Result<Vec<VersionRangeComponent>> {
207        // Simple implementation - can be extended for complex ranges
208        let mut components = Vec::new();
209
210        // Handle single constraint for now
211        let (operator, version_str) = if expression.starts_with(">=") {
212            (VersionOperator::GreaterThanOrEqual, &expression[2..])
213        } else if expression.starts_with("<=") {
214            (VersionOperator::LessThanOrEqual, &expression[2..])
215        } else if expression.starts_with('>') {
216            (VersionOperator::GreaterThan, &expression[1..])
217        } else if expression.starts_with('<') {
218            (VersionOperator::LessThan, &expression[1..])
219        } else if expression.starts_with('^') {
220            (VersionOperator::Caret, &expression[1..])
221        } else if expression.starts_with('~') {
222            (VersionOperator::Tilde, &expression[1..])
223        } else if expression.starts_with('=') {
224            (VersionOperator::Equal, &expression[1..])
225        } else {
226            (VersionOperator::Equal, expression)
227        };
228
229        let version = Version::parse(version_str.trim())?;
230        components.push(VersionRangeComponent { operator, version });
231
232        Ok(components)
233    }
234}
235
236impl VersionRangeComponent {
237    /// Check if a version satisfies this component
238    pub fn satisfies(&self, version: &Version) -> bool {
239        match self.operator {
240            VersionOperator::Equal => version == &self.version,
241            VersionOperator::GreaterThan => version > &self.version,
242            VersionOperator::GreaterThanOrEqual => version >= &self.version,
243            VersionOperator::LessThan => version < &self.version,
244            VersionOperator::LessThanOrEqual => version <= &self.version,
245            VersionOperator::Caret => self.satisfies_caret(version),
246            VersionOperator::Tilde => self.satisfies_tilde(version),
247        }
248    }
249
250    fn satisfies_caret(&self, version: &Version) -> bool {
251        // ^1.2.3 := >=1.2.3 <2.0.0 (compatible release)
252        if version < &self.version {
253            return false;
254        }
255
256        if self.version.major == 0 {
257            // ^0.2.3 := >=0.2.3 <0.3.0
258            version.major == self.version.major && version.minor == self.version.minor
259        } else {
260            // ^1.2.3 := >=1.2.3 <2.0.0
261            version.major == self.version.major
262        }
263    }
264
265    fn satisfies_tilde(&self, version: &Version) -> bool {
266        // ~1.2.3 := >=1.2.3 <1.3.0 (patch updates only)
267        version >= &self.version
268            && version.major == self.version.major
269            && version.minor == self.version.minor
270    }
271}
272
273impl VersionMatcher {
274    /// Check if a version satisfies a constraint expression
275    pub fn matches(version_str: &str, constraint: &str) -> Result<bool> {
276        let version = Version::parse(version_str)?;
277        let range = VersionRange::parse(constraint)?;
278        Ok(range.satisfies(&version))
279    }
280
281    /// Find the best matching version from a list
282    pub fn find_best_match(versions: &[String], constraint: &str) -> Result<Option<String>> {
283        let range = VersionRange::parse(constraint)?;
284        let mut matching_versions = Vec::new();
285
286        for version_str in versions {
287            if let Ok(version) = Version::parse(version_str) {
288                if range.satisfies(&version) {
289                    matching_versions.push((version, version_str.clone()));
290                }
291            }
292        }
293
294        // Sort by version (highest first) and return the best match
295        matching_versions.sort_by(|a, b| b.0.cmp(&a.0));
296        Ok(matching_versions
297            .first()
298            .map(|(_, version_str)| version_str.clone()))
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_version_parsing() {
308        let v = Version::parse("1.2.3").unwrap();
309        assert_eq!(v.major, 1);
310        assert_eq!(v.minor, 2);
311        assert_eq!(v.patch, 3);
312        assert!(v.prerelease.is_none());
313
314        let v_pre = Version::parse("2.0.0-beta.1").unwrap();
315        assert_eq!(v_pre.major, 2);
316        assert_eq!(v_pre.prerelease, Some("beta.1".to_string()));
317        assert!(v_pre.is_prerelease());
318
319        let v_build = Version::parse("1.0.0+build.123").unwrap();
320        assert_eq!(v_build.build, Some("build.123".to_string()));
321    }
322
323    #[test]
324    fn test_version_comparison() {
325        let v1 = Version::parse("1.0.0").unwrap();
326        let v2 = Version::parse("1.0.1").unwrap();
327        let v3 = Version::parse("1.1.0").unwrap();
328        let v_pre = Version::parse("1.0.0-beta").unwrap();
329
330        assert!(v1 < v2);
331        assert!(v2 < v3);
332        assert!(v_pre < v1); // Prerelease < release
333    }
334
335    #[test]
336    fn test_version_range_parsing() {
337        let range = VersionRange::parse(">=1.0.0").unwrap();
338        assert_eq!(range.components.len(), 1);
339        assert_eq!(
340            range.components[0].operator,
341            VersionOperator::GreaterThanOrEqual
342        );
343
344        let caret_range = VersionRange::parse("^1.2.3").unwrap();
345        assert_eq!(caret_range.components[0].operator, VersionOperator::Caret);
346    }
347
348    #[test]
349    fn test_version_matching() {
350        assert!(VersionMatcher::matches("1.2.3", ">=1.0.0").unwrap());
351        assert!(!VersionMatcher::matches("0.9.0", ">=1.0.0").unwrap());
352
353        assert!(VersionMatcher::matches("1.2.5", "^1.2.3").unwrap());
354        assert!(!VersionMatcher::matches("2.0.0", "^1.2.3").unwrap());
355
356        assert!(VersionMatcher::matches("1.2.5", "~1.2.3").unwrap());
357        assert!(!VersionMatcher::matches("1.3.0", "~1.2.3").unwrap());
358    }
359
360    #[test]
361    fn test_find_best_match() {
362        let versions = vec![
363            "1.0.0".to_string(),
364            "1.1.0".to_string(),
365            "1.2.0".to_string(),
366            "2.0.0".to_string(),
367        ];
368
369        let best = VersionMatcher::find_best_match(&versions, "^1.0.0").unwrap();
370        assert_eq!(best, Some("1.2.0".to_string())); // Highest compatible version
371
372        let best_exact = VersionMatcher::find_best_match(&versions, "=1.1.0").unwrap();
373        assert_eq!(best_exact, Some("1.1.0".to_string()));
374    }
375}