robinpath_modules/modules/
semver_mod.rs1use 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 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 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 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 let conditions: Vec<&str> = range.split_whitespace().collect();
272 if conditions.len() > 1 {
273 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 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 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}