codex_patcher/config/
version.rs1use semver::{Version, VersionReq};
7use std::fmt;
8
9#[derive(Debug, Clone)]
11pub enum VersionError {
12 InvalidVersion { value: String, source: String },
14 InvalidRequirement { value: String, source: String },
16}
17
18impl fmt::Display for VersionError {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 VersionError::InvalidVersion { value, source } => {
22 write!(f, "invalid version '{}': {}", value, source)
23 }
24 VersionError::InvalidRequirement { value, source } => {
25 write!(f, "invalid version requirement '{}': {}", value, source)
26 }
27 }
28 }
29}
30
31impl std::error::Error for VersionError {}
32
33pub fn matches_requirement(version: &str, requirement: Option<&str>) -> Result<bool, VersionError> {
48 let Some(req_str) = requirement else {
50 return Ok(true);
51 };
52
53 let req_str = req_str.trim();
55 if req_str.is_empty() {
56 return Ok(true);
57 }
58
59 let version = Version::parse(version).map_err(|e| VersionError::InvalidVersion {
61 value: version.to_string(),
62 source: e.to_string(),
63 })?;
64
65 let req = VersionReq::parse(req_str).map_err(|e| VersionError::InvalidRequirement {
67 value: req_str.to_string(),
68 source: e.to_string(),
69 })?;
70
71 if req.matches(&version) {
72 return Ok(true);
73 }
74
75 if !version.pre.is_empty() {
93 let dominated = req.comparators.iter().all(|c| {
107 let c_minor = c.minor.unwrap_or(0);
108 let c_patch = c.patch.unwrap_or(0);
109 matches!(c.op, semver::Op::Less | semver::Op::LessEq)
111 || (version.major, version.minor, version.patch) > (c.major, c_minor, c_patch)
112 });
113 if dominated {
114 let exceeds_prerelease_upper = req.comparators.iter().any(|c| {
115 matches!(c.op, semver::Op::Less | semver::Op::LessEq)
116 && !c.pre.is_empty()
117 && {
118 let c_minor = c.minor.unwrap_or(0);
119 let c_patch = c.patch.unwrap_or(0);
120 (version.major, version.minor, version.patch) > (c.major, c_minor, c_patch)
121 }
122 });
123 if !exceeds_prerelease_upper {
124 let base = Version::new(version.major, version.minor, version.patch);
125 if req.matches(&base) {
126 return Ok(true);
127 }
128 }
129 }
130 }
131
132 Ok(false)
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_no_requirement() {
141 assert!(matches_requirement("0.88.0", None).unwrap());
142 assert!(matches_requirement("1.0.0", None).unwrap());
143 assert!(matches_requirement("0.1.0", None).unwrap());
144 }
145
146 #[test]
147 fn test_empty_requirement() {
148 assert!(matches_requirement("0.88.0", Some("")).unwrap());
149 assert!(matches_requirement("1.0.0", Some(" ")).unwrap());
150 }
151
152 #[test]
153 fn test_simple_requirement() {
154 assert!(matches_requirement("0.88.0", Some("=0.88.0")).unwrap());
156 assert!(!matches_requirement("0.88.1", Some("=0.88.0")).unwrap());
157
158 assert!(matches_requirement("0.88.0", Some(">=0.88.0")).unwrap());
160 assert!(matches_requirement("0.89.0", Some(">=0.88.0")).unwrap());
161 assert!(!matches_requirement("0.87.0", Some(">=0.88.0")).unwrap());
162
163 assert!(matches_requirement("0.87.0", Some("<0.88.0")).unwrap());
165 assert!(!matches_requirement("0.88.0", Some("<0.88.0")).unwrap());
166 }
167
168 #[test]
169 fn test_compound_requirement() {
170 let req = ">=0.88.0, <0.90.0";
171
172 assert!(matches_requirement("0.88.0", Some(req)).unwrap());
173 assert!(matches_requirement("0.89.0", Some(req)).unwrap());
174 assert!(matches_requirement("0.89.5", Some(req)).unwrap());
175 assert!(!matches_requirement("0.87.0", Some(req)).unwrap());
176 assert!(!matches_requirement("0.90.0", Some(req)).unwrap());
177 assert!(!matches_requirement("1.0.0", Some(req)).unwrap());
178 }
179
180 #[test]
181 fn test_caret_requirement() {
182 let req = "^0.88";
184 assert!(matches_requirement("0.88.0", Some(req)).unwrap());
185 assert!(matches_requirement("0.88.5", Some(req)).unwrap());
186 assert!(!matches_requirement("0.89.0", Some(req)).unwrap());
187 assert!(!matches_requirement("0.87.0", Some(req)).unwrap());
188 }
189
190 #[test]
191 fn test_tilde_requirement() {
192 let req = "~0.88.0";
194 assert!(matches_requirement("0.88.0", Some(req)).unwrap());
195 assert!(matches_requirement("0.88.9", Some(req)).unwrap());
196 assert!(!matches_requirement("0.89.0", Some(req)).unwrap());
197 }
198
199 #[test]
200 fn test_invalid_version() {
201 let result = matches_requirement("not-a-version", Some(">=0.88.0"));
202 assert!(result.is_err());
203 assert!(matches!(
204 result.unwrap_err(),
205 VersionError::InvalidVersion { .. }
206 ));
207 }
208
209 #[test]
210 fn test_invalid_requirement() {
211 let result = matches_requirement("0.88.0", Some(">=bad-version"));
212 assert!(result.is_err());
213 assert!(matches!(
214 result.unwrap_err(),
215 VersionError::InvalidRequirement { .. }
216 ));
217 }
218
219 #[test]
220 fn test_prerelease_versions() {
221 let req = ">=0.88.0-alpha.4";
222 assert!(matches_requirement("0.88.0-alpha.4", Some(req)).unwrap());
223 assert!(matches_requirement("0.88.0-alpha.5", Some(req)).unwrap());
224 assert!(matches_requirement("0.88.0", Some(req)).unwrap());
225 assert!(!matches_requirement("0.88.0-alpha.3", Some(req)).unwrap());
226 }
227
228 #[test]
229 fn test_prerelease_cross_minor_fallback() {
230 assert!(matches_requirement("0.100.0-alpha.2", Some(">=0.99.0-alpha.21")).unwrap());
235 assert!(matches_requirement("0.100.0-alpha.2", Some(">=0.92.0")).unwrap());
236 assert!(matches_requirement("0.100.0-alpha.2", Some(">=0.88.0")).unwrap());
237 assert!(matches_requirement("0.100.0-alpha.2", Some(">=0.99.0-alpha.0")).unwrap());
238
239 assert!(!matches_requirement(
241 "0.100.0-alpha.2",
242 Some(">=0.99.0-alpha.2, <0.99.0-alpha.10")
243 )
244 .unwrap());
245 assert!(
246 !matches_requirement("0.100.0-alpha.2", Some(">=0.88.0, <0.99.0-alpha.7")).unwrap()
247 );
248
249 assert!(!matches_requirement("0.99.0-alpha.12", Some(">=0.99.0-alpha.21")).unwrap());
251 assert!(matches_requirement("0.99.0-alpha.22", Some(">=0.99.0-alpha.21")).unwrap());
252 }
253
254 #[test]
255 fn test_prerelease_upper_bound_not_exceeded() {
256 let req = ">=0.105.0-alpha.13, <0.108.0-alpha.1";
263 assert!(!matches_requirement("0.112.0-alpha.11", Some(req)).unwrap());
264 assert!(!matches_requirement("0.108.0", Some(req)).unwrap());
265 assert!(!matches_requirement("0.109.0-alpha.1", Some(req)).unwrap());
266
267 assert!(matches_requirement("0.105.0-alpha.13", Some(req)).unwrap());
269 assert!(matches_requirement("0.106.0-alpha.5", Some(req)).unwrap());
270 assert!(matches_requirement("0.107.0", Some(req)).unwrap());
271
272 assert!(matches_requirement("0.112.0-alpha.11", Some(">=0.105.0-alpha.13")).unwrap());
275 }
276}