Skip to main content

claude_wrapper/
version.rs

1use std::fmt;
2use std::str::FromStr;
3
4/// A parsed Claude CLI version (semver).
5///
6/// # Example
7///
8/// ```
9/// use claude_wrapper::CliVersion;
10///
11/// let v: CliVersion = "2.1.71".parse().unwrap();
12/// assert_eq!(v.major, 2);
13/// assert_eq!(v.minor, 1);
14/// assert_eq!(v.patch, 71);
15///
16/// let min: CliVersion = "2.1.0".parse().unwrap();
17/// assert!(v >= min);
18/// ```
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct CliVersion {
21    pub major: u32,
22    pub minor: u32,
23    pub patch: u32,
24}
25
26impl CliVersion {
27    /// Create a new version.
28    #[must_use]
29    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
30        Self {
31            major,
32            minor,
33            patch,
34        }
35    }
36
37    /// Parse a version from the output of `claude --version`.
38    ///
39    /// Expects format like `"2.1.71 (Claude Code)"` or just `"2.1.71"`.
40    pub fn parse_version_output(output: &str) -> Result<Self, VersionParseError> {
41        let version_str = output.split_whitespace().next().unwrap_or("");
42        version_str.parse()
43    }
44
45    /// Check if this version satisfies a minimum version requirement.
46    #[must_use]
47    pub fn satisfies_minimum(&self, minimum: &CliVersion) -> bool {
48        self >= minimum
49    }
50}
51
52impl PartialOrd for CliVersion {
53    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
54        Some(self.cmp(other))
55    }
56}
57
58impl Ord for CliVersion {
59    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
60        self.major
61            .cmp(&other.major)
62            .then(self.minor.cmp(&other.minor))
63            .then(self.patch.cmp(&other.patch))
64    }
65}
66
67impl fmt::Display for CliVersion {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
70    }
71}
72
73impl FromStr for CliVersion {
74    type Err = VersionParseError;
75
76    fn from_str(s: &str) -> Result<Self, Self::Err> {
77        let parts: Vec<&str> = s.split('.').collect();
78        if parts.len() != 3 {
79            return Err(VersionParseError(s.to_string()));
80        }
81
82        let major = parts[0]
83            .parse()
84            .map_err(|_| VersionParseError(s.to_string()))?;
85        let minor = parts[1]
86            .parse()
87            .map_err(|_| VersionParseError(s.to_string()))?;
88        let patch = parts[2]
89            .parse()
90            .map_err(|_| VersionParseError(s.to_string()))?;
91
92        Ok(Self {
93            major,
94            minor,
95            patch,
96        })
97    }
98}
99
100/// Error returned when a version string cannot be parsed.
101#[derive(Debug, Clone, thiserror::Error)]
102#[error("invalid version string: {0:?}")]
103pub struct VersionParseError(pub String);
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_parse_simple() {
111        let v: CliVersion = "2.1.71".parse().unwrap();
112        assert_eq!(v.major, 2);
113        assert_eq!(v.minor, 1);
114        assert_eq!(v.patch, 71);
115    }
116
117    #[test]
118    fn test_parse_version_output() {
119        let v = CliVersion::parse_version_output("2.1.71 (Claude Code)").unwrap();
120        assert_eq!(v, CliVersion::new(2, 1, 71));
121    }
122
123    #[test]
124    fn test_parse_version_output_trimmed() {
125        let v = CliVersion::parse_version_output("  2.1.71 (Claude Code)\n").unwrap();
126        assert_eq!(v, CliVersion::new(2, 1, 71));
127    }
128
129    #[test]
130    fn test_display() {
131        let v = CliVersion::new(2, 1, 71);
132        assert_eq!(v.to_string(), "2.1.71");
133    }
134
135    #[test]
136    fn test_ordering() {
137        let v1 = CliVersion::new(2, 0, 0);
138        let v2 = CliVersion::new(2, 1, 0);
139        let v3 = CliVersion::new(2, 1, 71);
140        let v4 = CliVersion::new(3, 0, 0);
141
142        assert!(v1 < v2);
143        assert!(v2 < v3);
144        assert!(v3 < v4);
145        assert!(v1 < v4);
146    }
147
148    #[test]
149    fn test_satisfies_minimum() {
150        let v = CliVersion::new(2, 1, 71);
151        assert!(v.satisfies_minimum(&CliVersion::new(2, 0, 0)));
152        assert!(v.satisfies_minimum(&CliVersion::new(2, 1, 71)));
153        assert!(!v.satisfies_minimum(&CliVersion::new(2, 2, 0)));
154        assert!(!v.satisfies_minimum(&CliVersion::new(3, 0, 0)));
155    }
156
157    #[test]
158    fn test_parse_invalid() {
159        assert!("not-a-version".parse::<CliVersion>().is_err());
160        assert!("2.1".parse::<CliVersion>().is_err());
161        assert!("2.1.x".parse::<CliVersion>().is_err());
162    }
163}