Skip to main content

robinpath_modules/modules/
semver_mod.rs

1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4    rp.register_builtin("semver.parse", |args, _| {
5        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
6        match parse_semver(&s) {
7            Some(sv) => {
8                let mut obj = indexmap::IndexMap::new();
9                obj.insert("major".to_string(), Value::Number(sv.major as f64));
10                obj.insert("minor".to_string(), Value::Number(sv.minor as f64));
11                obj.insert("patch".to_string(), Value::Number(sv.patch as f64));
12                obj.insert("prerelease".to_string(), match &sv.prerelease {
13                    Some(p) => Value::String(p.clone()),
14                    None => Value::Null,
15                });
16                obj.insert("build".to_string(), match &sv.build {
17                    Some(b) => Value::String(b.clone()),
18                    None => Value::Null,
19                });
20                Ok(Value::Object(obj))
21            }
22            None => Ok(Value::Null),
23        }
24    });
25
26    rp.register_builtin("semver.isValid", |args, _| {
27        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
28        Ok(Value::Bool(parse_semver(&s).is_some()))
29    });
30
31    rp.register_builtin("semver.compare", |args, _| {
32        let v1 = args.first().map(|v| v.to_display_string()).unwrap_or_default();
33        let v2 = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
34        match (parse_semver(&v1), parse_semver(&v2)) {
35            (Some(a), Some(b)) => Ok(Value::Number(compare_semver(&a, &b) as f64)),
36            _ => Ok(Value::Null),
37        }
38    });
39
40    rp.register_builtin("semver.gt", |args, _| {
41        let v1 = args.first().map(|v| v.to_display_string()).unwrap_or_default();
42        let v2 = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
43        match (parse_semver(&v1), parse_semver(&v2)) {
44            (Some(a), Some(b)) => Ok(Value::Bool(compare_semver(&a, &b) > 0)),
45            _ => Ok(Value::Bool(false)),
46        }
47    });
48
49    rp.register_builtin("semver.lt", |args, _| {
50        let v1 = args.first().map(|v| v.to_display_string()).unwrap_or_default();
51        let v2 = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
52        match (parse_semver(&v1), parse_semver(&v2)) {
53            (Some(a), Some(b)) => Ok(Value::Bool(compare_semver(&a, &b) < 0)),
54            _ => Ok(Value::Bool(false)),
55        }
56    });
57
58    rp.register_builtin("semver.eq", |args, _| {
59        let v1 = args.first().map(|v| v.to_display_string()).unwrap_or_default();
60        let v2 = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
61        match (parse_semver(&v1), parse_semver(&v2)) {
62            (Some(a), Some(b)) => Ok(Value::Bool(compare_semver(&a, &b) == 0)),
63            _ => Ok(Value::Bool(false)),
64        }
65    });
66
67    rp.register_builtin("semver.gte", |args, _| {
68        let v1 = args.first().map(|v| v.to_display_string()).unwrap_or_default();
69        let v2 = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
70        match (parse_semver(&v1), parse_semver(&v2)) {
71            (Some(a), Some(b)) => Ok(Value::Bool(compare_semver(&a, &b) >= 0)),
72            _ => Ok(Value::Bool(false)),
73        }
74    });
75
76    rp.register_builtin("semver.lte", |args, _| {
77        let v1 = args.first().map(|v| v.to_display_string()).unwrap_or_default();
78        let v2 = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
79        match (parse_semver(&v1), parse_semver(&v2)) {
80            (Some(a), Some(b)) => Ok(Value::Bool(compare_semver(&a, &b) <= 0)),
81            _ => Ok(Value::Bool(false)),
82        }
83    });
84
85    rp.register_builtin("semver.inc", |args, _| {
86        let v = args.first().map(|v| v.to_display_string()).unwrap_or_default();
87        let release = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "patch".to_string());
88        match parse_semver(&v) {
89            Some(mut sv) => {
90                match release.as_str() {
91                    "major" => { sv.major += 1; sv.minor = 0; sv.patch = 0; sv.prerelease = None; }
92                    "minor" => { sv.minor += 1; sv.patch = 0; sv.prerelease = None; }
93                    "patch" => { sv.patch += 1; sv.prerelease = None; }
94                    "prerelease" => {
95                        match &sv.prerelease {
96                            Some(p) => {
97                                // Try to increment number at end
98                                if let Some(n) = p.rsplit('.').next().and_then(|s| s.parse::<u64>().ok()) {
99                                    let prefix = &p[..p.rfind('.').map_or(0, |i| i + 1)];
100                                    sv.prerelease = Some(format!("{}{}", prefix, n + 1));
101                                } else {
102                                    sv.prerelease = Some(format!("{}.0", p));
103                                }
104                            }
105                            None => {
106                                sv.patch += 1;
107                                sv.prerelease = Some("0".to_string());
108                            }
109                        }
110                    }
111                    _ => sv.patch += 1,
112                }
113                sv.build = None;
114                Ok(Value::String(sv.to_string()))
115            }
116            None => Ok(Value::Null),
117        }
118    });
119
120    rp.register_builtin("semver.major", |args, _| {
121        let v = args.first().map(|v| v.to_display_string()).unwrap_or_default();
122        match parse_semver(&v) {
123            Some(sv) => Ok(Value::Number(sv.major as f64)),
124            None => Ok(Value::Null),
125        }
126    });
127
128    rp.register_builtin("semver.minor", |args, _| {
129        let v = args.first().map(|v| v.to_display_string()).unwrap_or_default();
130        match parse_semver(&v) {
131            Some(sv) => Ok(Value::Number(sv.minor as f64)),
132            None => Ok(Value::Null),
133        }
134    });
135
136    rp.register_builtin("semver.patch", |args, _| {
137        let v = args.first().map(|v| v.to_display_string()).unwrap_or_default();
138        match parse_semver(&v) {
139            Some(sv) => Ok(Value::Number(sv.patch as f64)),
140            None => Ok(Value::Null),
141        }
142    });
143
144    rp.register_builtin("semver.coerce", |args, _| {
145        let v = args.first().map(|v| v.to_display_string()).unwrap_or_default();
146        let cleaned = v.trim_start_matches('v').trim_start_matches('V');
147        let parts: Vec<&str> = cleaned.split('.').collect();
148        let major = parts.first().and_then(|s| s.parse::<u64>().ok()).unwrap_or(0);
149        let minor = parts.get(1).and_then(|s| s.parse::<u64>().ok()).unwrap_or(0);
150        let patch = parts.get(2).and_then(|s| s.split('-').next().and_then(|s| s.parse::<u64>().ok())).unwrap_or(0);
151        Ok(Value::String(format!("{}.{}.{}", major, minor, patch)))
152    });
153
154    rp.register_builtin("semver.diff", |args, _| {
155        let v1 = args.first().map(|v| v.to_display_string()).unwrap_or_default();
156        let v2 = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
157        match (parse_semver(&v1), parse_semver(&v2)) {
158            (Some(a), Some(b)) => {
159                if a.major != b.major {
160                    Ok(Value::String("major".to_string()))
161                } else if a.minor != b.minor {
162                    Ok(Value::String("minor".to_string()))
163                } else if a.patch != b.patch {
164                    Ok(Value::String("patch".to_string()))
165                } else if a.prerelease != b.prerelease {
166                    Ok(Value::String("prerelease".to_string()))
167                } else {
168                    Ok(Value::Null)
169                }
170            }
171            _ => Ok(Value::Null),
172        }
173    });
174
175    rp.register_builtin("semver.satisfies", |args, _| {
176        let version = args.first().map(|v| v.to_display_string()).unwrap_or_default();
177        let range = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
178        let sv = match parse_semver(&version) {
179            Some(v) => v,
180            None => return Ok(Value::Bool(false)),
181        };
182        Ok(Value::Bool(satisfies_range(&sv, &range)))
183    });
184}
185
186#[derive(Clone)]
187struct SemVer {
188    major: u64,
189    minor: u64,
190    patch: u64,
191    prerelease: Option<String>,
192    build: Option<String>,
193}
194
195impl std::fmt::Display for SemVer {
196    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
197        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
198        if let Some(ref pre) = self.prerelease {
199            write!(f, "-{}", pre)?;
200        }
201        if let Some(ref build) = self.build {
202            write!(f, "+{}", build)?;
203        }
204        Ok(())
205    }
206}
207
208fn parse_semver(s: &str) -> Option<SemVer> {
209    let s = s.trim().trim_start_matches('v').trim_start_matches('V');
210    if s.is_empty() {
211        return None;
212    }
213
214    let (version_part, build) = if let Some(pos) = s.find('+') {
215        (&s[..pos], Some(s[pos + 1..].to_string()))
216    } else {
217        (s, None)
218    };
219
220    let (version_part, prerelease) = if let Some(pos) = version_part.find('-') {
221        (&version_part[..pos], Some(version_part[pos + 1..].to_string()))
222    } else {
223        (version_part, None)
224    };
225
226    let parts: Vec<&str> = version_part.split('.').collect();
227    let major = parts.first()?.parse().ok()?;
228    let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
229    let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
230
231    Some(SemVer { major, minor, patch, prerelease, build })
232}
233
234fn compare_semver(a: &SemVer, b: &SemVer) -> i32 {
235    if a.major != b.major {
236        return if a.major > b.major { 1 } else { -1 };
237    }
238    if a.minor != b.minor {
239        return if a.minor > b.minor { 1 } else { -1 };
240    }
241    if a.patch != b.patch {
242        return if a.patch > b.patch { 1 } else { -1 };
243    }
244    // Prerelease: version without prerelease > version with prerelease
245    match (&a.prerelease, &b.prerelease) {
246        (None, None) => 0,
247        (None, Some(_)) => 1,
248        (Some(_), None) => -1,
249        (Some(pa), Some(pb)) => pa.cmp(pb) as i32,
250    }
251}
252
253fn satisfies_range(sv: &SemVer, range: &str) -> bool {
254    // Handle || (OR)
255    for part in range.split("||") {
256        let part = part.trim();
257        if satisfies_single(sv, part) {
258            return true;
259        }
260    }
261    false
262}
263
264fn satisfies_single(sv: &SemVer, range: &str) -> bool {
265    let range = range.trim();
266    if range.is_empty() || range == "*" {
267        return true;
268    }
269
270    // Handle space-separated AND conditions
271    let conditions: Vec<&str> = range.split_whitespace().collect();
272    if conditions.len() > 1 {
273        // Check if it's ">=X <Y" style
274        let mut i = 0;
275        while i < conditions.len() {
276            let cond = conditions[i];
277            if !satisfies_comparator(sv, cond) {
278                return false;
279            }
280            i += 1;
281        }
282        return true;
283    }
284
285    // ^ range (compatible)
286    if let Some(r) = range.strip_prefix('^') {
287        if let Some(target) = parse_semver(r) {
288            if sv.major != target.major {
289                return false;
290            }
291            return compare_semver(sv, &target) >= 0;
292        }
293        return false;
294    }
295
296    // ~ range (approximately)
297    if let Some(r) = range.strip_prefix('~') {
298        if let Some(target) = parse_semver(r) {
299            if sv.major != target.major || sv.minor != target.minor {
300                return false;
301            }
302            return sv.patch >= target.patch;
303        }
304        return false;
305    }
306
307    satisfies_comparator(sv, range)
308}
309
310fn satisfies_comparator(sv: &SemVer, cond: &str) -> bool {
311    if let Some(r) = cond.strip_prefix(">=") {
312        parse_semver(r).is_some_and(|t| compare_semver(sv, &t) >= 0)
313    } else if let Some(r) = cond.strip_prefix("<=") {
314        parse_semver(r).is_some_and(|t| compare_semver(sv, &t) <= 0)
315    } else if let Some(r) = cond.strip_prefix('>') {
316        parse_semver(r).is_some_and(|t| compare_semver(sv, &t) > 0)
317    } else if let Some(r) = cond.strip_prefix('<') {
318        parse_semver(r).is_some_and(|t| compare_semver(sv, &t) < 0)
319    } else if let Some(r) = cond.strip_prefix('=') {
320        parse_semver(r).is_some_and(|t| compare_semver(sv, &t) == 0)
321    } else {
322        parse_semver(cond).is_some_and(|t| compare_semver(sv, &t) == 0)
323    }
324}