Skip to main content

mai_cli/core/
version.rs

1use serde::{Deserialize, Serialize};
2use std::cmp::Ordering;
3use std::fmt;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
6pub struct Version {
7    pub major: u32,
8    pub minor: u32,
9    pub patch: u32,
10}
11
12impl Version {
13    #[allow(dead_code)]
14    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
15        Self {
16            major,
17            minor,
18            patch,
19        }
20    }
21
22    pub fn parse(s: &str) -> Result<Self, String> {
23        let parts: Vec<&str> = s.trim().split('.').collect();
24        if parts.len() != 3 {
25            return Err(format!(
26                "Invalid version format: {}. Expected: MAJOR.MINOR.PATCH",
27                s
28            ));
29        }
30        let major = parts[0]
31            .parse::<u32>()
32            .map_err(|e| format!("Invalid major: {}", e))?;
33        let minor = parts[1]
34            .parse::<u32>()
35            .map_err(|e| format!("Invalid minor: {}", e))?;
36        let patch = parts[2]
37            .parse::<u32>()
38            .map_err(|e| format!("Invalid patch: {}", e))?;
39        Ok(Self::new(major, minor, patch))
40    }
41
42    #[allow(dead_code)]
43    pub fn bump_major(&self) -> Self {
44        Self::new(self.major + 1, 0, 0)
45    }
46
47    #[allow(dead_code)]
48    pub fn bump_minor(&self) -> Self {
49        Self::new(self.major, self.minor + 1, 0)
50    }
51
52    #[allow(dead_code)]
53    pub fn bump_patch(&self) -> Self {
54        Self::new(self.major, self.minor, self.patch + 1)
55    }
56}
57
58impl fmt::Display for Version {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
61    }
62}
63
64impl PartialOrd for Version {
65    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
66        Some(self.cmp(other))
67    }
68}
69
70impl Ord for Version {
71    fn cmp(&self, other: &Self) -> Ordering {
72        self.major
73            .cmp(&other.major)
74            .then_with(|| self.minor.cmp(&other.minor))
75            .then_with(|| self.patch.cmp(&other.patch))
76    }
77}
78
79/// Version requirement specifier (semver-style ranges)
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum VersionReq {
82    /// Exact version: `1.2.3`
83    Exact(Version),
84    /// Latest version: `latest`
85    Latest,
86    /// Caret (compatible): `^1.2.3` (>=1.2.3, <2.0.0)
87    Caret(Version),
88    /// Tilde (patch-level): `~1.2.3` (>=1.2.3, <1.3.0)
89    Tilde(Version),
90    /// Greater than: `>1.2.3`
91    GreaterThan(Version),
92    /// Greater or equal: `>=1.2.3`
93    GreaterEq(Version),
94    /// Less than: `<1.2.3`
95    LessThan(Version),
96    /// Less or equal: `<=1.2.3`
97    LessEq(Version),
98    /// Range: `>=1.0.0, <2.0.0`
99    Range {
100        min: Option<Version>,
101        max: Option<Version>,
102    },
103}
104
105impl VersionReq {
106    pub fn parse(s: &str) -> Result<Self, String> {
107        let s = s.trim();
108
109        if s.eq_ignore_ascii_case("latest") || s == "*" {
110            return Ok(VersionReq::Latest);
111        }
112
113        // Handle range with comma: `>=1.0.0, <2.0.0`
114        if let Some(pos) = s.find(',') {
115            let left = s[..pos].trim();
116            let right = s[pos + 1..].trim();
117            let min = if left.is_empty() {
118                None
119            } else {
120                match Self::parse_single(left)? {
121                    VersionReq::GreaterEq(v) => Some(v),
122                    VersionReq::GreaterThan(v) => Some(v),
123                    VersionReq::Exact(v) => Some(v),
124                    _ => None,
125                }
126            };
127            let max = if right.is_empty() {
128                None
129            } else {
130                match Self::parse_single(right)? {
131                    VersionReq::LessThan(v) => Some(v),
132                    VersionReq::LessEq(v) => Some(v),
133                    VersionReq::Exact(v) => Some(v),
134                    _ => None,
135                }
136            };
137            return Ok(VersionReq::Range { min, max });
138        }
139
140        Self::parse_single(s)
141    }
142
143    fn parse_single(s: &str) -> Result<Self, String> {
144        let s = s.trim();
145
146        if let Some(v) = s.strip_prefix("^") {
147            Ok(VersionReq::Caret(Version::parse(v)?))
148        } else if let Some(v) = s.strip_prefix("~") {
149            Ok(VersionReq::Tilde(Version::parse(v)?))
150        } else if let Some(v) = s.strip_prefix(">=") {
151            Ok(VersionReq::GreaterEq(Version::parse(v)?))
152        } else if let Some(v) = s.strip_prefix(">") {
153            Ok(VersionReq::GreaterThan(Version::parse(v)?))
154        } else if let Some(v) = s.strip_prefix("<=") {
155            Ok(VersionReq::LessEq(Version::parse(v)?))
156        } else if let Some(v) = s.strip_prefix("<") {
157            Ok(VersionReq::LessThan(Version::parse(v)?))
158        } else {
159            Ok(VersionReq::Exact(Version::parse(s)?))
160        }
161    }
162
163    /// Check if a version satisfies this requirement
164    pub fn matches(&self, version: &Version) -> bool {
165        match self {
166            VersionReq::Exact(v) => version == v,
167            VersionReq::Latest => true,
168            VersionReq::Caret(v) => {
169                // ^1.2.3 means >=1.2.3, <2.0.0
170                version >= v && version.major == v.major
171            }
172            VersionReq::Tilde(v) => {
173                // ~1.2.3 means >=1.2.3, <1.3.0
174                version >= v && version.major == v.major && version.minor == v.minor
175            }
176            VersionReq::GreaterThan(v) => version > v,
177            VersionReq::GreaterEq(v) => version >= v,
178            VersionReq::LessThan(v) => version < v,
179            VersionReq::LessEq(v) => version <= v,
180            VersionReq::Range { min, max } => {
181                let min_ok = min.as_ref().is_none_or(|m| version >= m);
182                let max_ok = max.as_ref().is_none_or(|m| version < m);
183                min_ok && max_ok
184            }
185        }
186    }
187
188    /// Select the best matching version from available versions
189    pub fn select_best<'a>(&self, available: &'a [Version]) -> Option<&'a Version> {
190        let mut candidates: Vec<&Version> = available.iter().filter(|v| self.matches(v)).collect();
191
192        if candidates.is_empty() {
193            return None;
194        }
195
196        candidates.sort();
197        candidates.into_iter().next_back()
198    }
199}
200
201impl fmt::Display for VersionReq {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        match self {
204            VersionReq::Exact(v) => write!(f, "{}", v),
205            VersionReq::Latest => write!(f, "latest"),
206            VersionReq::Caret(v) => write!(f, "^{}", v),
207            VersionReq::Tilde(v) => write!(f, "~{}", v),
208            VersionReq::GreaterThan(v) => write!(f, ">{}", v),
209            VersionReq::GreaterEq(v) => write!(f, ">={}", v),
210            VersionReq::LessThan(v) => write!(f, "<{}", v),
211            VersionReq::LessEq(v) => write!(f, "<={}", v),
212            VersionReq::Range { min, max } => match (min, max) {
213                (Some(min), Some(max)) => write!(f, ">={}, <{}", min, max),
214                (Some(min), None) => write!(f, ">={}", min),
215                (None, Some(max)) => write!(f, "<{}", max),
216                (None, None) => write!(f, "*"),
217            },
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_version_parse() {
228        let v = Version::parse("1.2.3").unwrap();
229        assert_eq!(v, Version::new(1, 2, 3));
230    }
231
232    #[test]
233    fn test_version_cmp() {
234        assert!(Version::new(2, 0, 0) > Version::new(1, 9, 9));
235        assert!(Version::new(1, 10, 0) > Version::new(1, 9, 9));
236    }
237
238    #[test]
239    fn test_version_req_parse() {
240        assert_eq!(
241            VersionReq::parse("1.2.3").unwrap(),
242            VersionReq::Exact(Version::new(1, 2, 3))
243        );
244        assert_eq!(VersionReq::parse("latest").unwrap(), VersionReq::Latest);
245        assert_eq!(
246            VersionReq::parse("^1.2.3").unwrap(),
247            VersionReq::Caret(Version::new(1, 2, 3))
248        );
249        assert_eq!(
250            VersionReq::parse("~1.2.3").unwrap(),
251            VersionReq::Tilde(Version::new(1, 2, 3))
252        );
253        assert_eq!(
254            VersionReq::parse(">1.2.3").unwrap(),
255            VersionReq::GreaterThan(Version::new(1, 2, 3))
256        );
257        assert_eq!(
258            VersionReq::parse(">=1.2.3").unwrap(),
259            VersionReq::GreaterEq(Version::new(1, 2, 3))
260        );
261        assert_eq!(
262            VersionReq::parse("<1.2.3").unwrap(),
263            VersionReq::LessThan(Version::new(1, 2, 3))
264        );
265        assert_eq!(
266            VersionReq::parse("<=1.2.3").unwrap(),
267            VersionReq::LessEq(Version::new(1, 2, 3))
268        );
269    }
270
271    #[test]
272    fn test_version_req_matches() {
273        let v123 = Version::new(1, 2, 3);
274        let v124 = Version::new(1, 2, 4);
275        let v130 = Version::new(1, 3, 0);
276        let v200 = Version::new(2, 0, 0);
277
278        assert!(VersionReq::Exact(v123.clone()).matches(&v123));
279        assert!(!VersionReq::Exact(v123.clone()).matches(&v124));
280
281        assert!(VersionReq::Caret(v123.clone()).matches(&v123));
282        assert!(VersionReq::Caret(v123.clone()).matches(&v124));
283        assert!(!VersionReq::Caret(v123.clone()).matches(&v200));
284
285        assert!(VersionReq::Tilde(v123.clone()).matches(&v123));
286        assert!(VersionReq::Tilde(v123.clone()).matches(&v124));
287        assert!(!VersionReq::Tilde(v123.clone()).matches(&v130));
288
289        assert!(VersionReq::GreaterThan(v123.clone()).matches(&v124));
290        assert!(!VersionReq::GreaterThan(v123.clone()).matches(&v123));
291
292        assert!(VersionReq::GreaterEq(v123.clone()).matches(&v123));
293        assert!(VersionReq::GreaterEq(v123.clone()).matches(&v124));
294
295        assert!(VersionReq::LessThan(v123.clone()).matches(&Version::new(1, 2, 2)));
296        assert!(!VersionReq::LessThan(v123.clone()).matches(&v123));
297    }
298
299    #[test]
300    fn test_version_req_select_best() {
301        let versions = vec![
302            Version::new(1, 0, 0),
303            Version::new(1, 1, 0),
304            Version::new(1, 2, 0),
305            Version::new(1, 2, 3),
306            Version::new(2, 0, 0),
307        ];
308
309        let req = VersionReq::parse("^1.2.0").unwrap();
310        let best = req.select_best(&versions);
311        assert_eq!(best, Some(&Version::new(1, 2, 3)));
312
313        let req = VersionReq::parse("latest").unwrap();
314        let best = req.select_best(&versions);
315        assert_eq!(best, Some(&Version::new(2, 0, 0)));
316    }
317}