1use thiserror::Error;
11
12#[derive(Debug, Error, PartialEq, Eq)]
14pub enum VersionError {
15 #[error(
17 "version too old: installed {installed} ({installed_major_minor}), minimum required {required_major_minor}"
18 )]
19 TooOld {
20 installed: String,
21 installed_major_minor: String,
22 required_major_minor: String,
23 },
24
25 #[error("invalid semver version: {0:?}")]
27 Invalid(String),
28}
29
30pub fn check_minimum_version(installed: &str, required: &str) -> Result<(), VersionError> {
35 let inst_norm = normalize(installed);
36 let (inst_major, inst_minor) =
37 major_minor(&inst_norm).ok_or_else(|| VersionError::Invalid(installed.to_string()))?;
38
39 let req_norm = normalize(required);
40 let (req_major, req_minor) =
41 major_minor(&req_norm).ok_or_else(|| VersionError::Invalid(required.to_string()))?;
42
43 if (inst_major, inst_minor) < (req_major, req_minor) {
44 return Err(VersionError::TooOld {
45 installed: installed.trim().to_string(),
46 installed_major_minor: format!("v{inst_major}.{inst_minor}"),
47 required_major_minor: format!("v{req_major}.{req_minor}"),
48 });
49 }
50
51 Ok(())
52}
53
54fn normalize(raw: &str) -> String {
56 let s = raw.trim();
57
58 let s = match s.rfind(' ') {
59 Some(idx) => &s[idx + 1..],
60 None => s,
61 };
62
63 if s.starts_with('v') {
64 s.to_string()
65 } else {
66 format!("v{s}")
67 }
68}
69
70fn major_minor(v: &str) -> Option<(u64, u64)> {
75 let rest = v.strip_prefix('v')?;
76 if rest.is_empty() {
77 return None;
78 }
79
80 let mut parts = rest.split('.');
81 let major_str = parts.next()?;
82 let minor_str = parts.next()?;
83 parts.next()?;
87
88 if major_str.is_empty() || minor_str.is_empty() {
89 return None;
90 }
91
92 let major = major_str.parse::<u64>().ok()?;
93 let minor = minor_str.parse::<u64>().ok()?;
94
95 Some((major, minor))
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 struct Case {
103 name: &'static str,
104 installed: &'static str,
105 required: &'static str,
106 want_err: WantErr,
107 }
108
109 enum WantErr {
110 None,
111 TooOld,
112 Invalid,
113 }
114
115 #[test]
118 fn check_minimum_version_cases() {
119 let cases = [
120 Case {
121 name: "same version",
122 installed: "mirrord 3.187.0",
123 required: "3.187.0",
124 want_err: WantErr::None,
125 },
126 Case {
127 name: "newer installed",
128 installed: "mirrord 3.200.0",
129 required: "3.187.0",
130 want_err: WantErr::None,
131 },
132 Case {
133 name: "older major",
134 installed: "mirrord 2.100.0",
135 required: "3.187.0",
136 want_err: WantErr::TooOld,
137 },
138 Case {
139 name: "older minor",
140 installed: "mirrord 3.100.0",
141 required: "3.187.0",
142 want_err: WantErr::TooOld,
143 },
144 Case {
145 name: "skaffold v prefix",
146 installed: "v2.17.3",
147 required: "2.17.3",
148 want_err: WantErr::None,
149 },
150 Case {
151 name: "skaffold too old",
152 installed: "v1.39.0",
153 required: "2.17.3",
154 want_err: WantErr::TooOld,
155 },
156 Case {
157 name: "newer patch same major minor",
158 installed: "v2.17.5",
159 required: "2.17.3",
160 want_err: WantErr::None,
161 },
162 Case {
163 name: "invalid installed version",
164 installed: "garbage",
165 required: "2.17.3",
166 want_err: WantErr::Invalid,
167 },
168 Case {
169 name: "invalid required version",
170 installed: "v2.17.3",
171 required: "not-a-version",
172 want_err: WantErr::Invalid,
173 },
174 Case {
175 name: "older patch same major minor passes",
176 installed: "v2.17.1",
177 required: "2.17.3",
178 want_err: WantErr::None,
179 },
180 ];
181
182 for case in cases {
183 let result = check_minimum_version(case.installed, case.required);
184
185 match case.want_err {
186 WantErr::None => assert!(result.is_ok(), "{}", case.name),
187 WantErr::TooOld => assert!(
188 matches!(result, Err(VersionError::TooOld { .. })),
189 "{}",
190 case.name
191 ),
192 WantErr::Invalid => assert!(
193 matches!(result, Err(VersionError::Invalid(_))),
194 "{}",
195 case.name
196 ),
197 }
198 }
199 }
200}