Skip to main content

jpx_core/extensions/
semver_fns.rs

1//! Semantic versioning functions.
2
3use std::collections::HashSet;
4
5use semver_crate::{Version, VersionReq};
6use serde_json::{Number, Value};
7
8use crate::functions::{Function, number_value};
9use crate::interpreter::SearchResult;
10use crate::registry::register_if_enabled;
11use crate::{Context, Runtime, arg, defn};
12
13// =============================================================================
14// semver_parse(s) -> object
15// =============================================================================
16
17defn!(SemverParseFn, vec![arg!(string)], None);
18
19impl Function for SemverParseFn {
20    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
21        self.signature.validate(args, ctx)?;
22        let s = args[0].as_str().unwrap();
23
24        match Version::parse(s) {
25            Ok(v) => {
26                let pre = if v.pre.is_empty() {
27                    Value::Null
28                } else {
29                    Value::String(v.pre.to_string())
30                };
31                let build = if v.build.is_empty() {
32                    Value::Null
33                } else {
34                    Value::String(v.build.to_string())
35                };
36
37                let obj = serde_json::json!({
38                    "major": v.major,
39                    "minor": v.minor,
40                    "patch": v.patch,
41                    "pre": pre,
42                    "build": build
43                });
44
45                Ok(obj)
46            }
47            Err(_) => Ok(Value::Null),
48        }
49    }
50}
51
52// =============================================================================
53// semver_major(s) -> number
54// =============================================================================
55
56defn!(SemverMajorFn, vec![arg!(string)], None);
57
58impl Function for SemverMajorFn {
59    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
60        self.signature.validate(args, ctx)?;
61        let s = args[0].as_str().unwrap();
62
63        match Version::parse(s) {
64            Ok(v) => Ok(Value::Number(Number::from(v.major))),
65            Err(_) => Ok(Value::Null),
66        }
67    }
68}
69
70// =============================================================================
71// semver_minor(s) -> number
72// =============================================================================
73
74defn!(SemverMinorFn, vec![arg!(string)], None);
75
76impl Function for SemverMinorFn {
77    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
78        self.signature.validate(args, ctx)?;
79        let s = args[0].as_str().unwrap();
80
81        match Version::parse(s) {
82            Ok(v) => Ok(Value::Number(Number::from(v.minor))),
83            Err(_) => Ok(Value::Null),
84        }
85    }
86}
87
88// =============================================================================
89// semver_patch(s) -> number
90// =============================================================================
91
92defn!(SemverPatchFn, vec![arg!(string)], None);
93
94impl Function for SemverPatchFn {
95    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
96        self.signature.validate(args, ctx)?;
97        let s = args[0].as_str().unwrap();
98
99        match Version::parse(s) {
100            Ok(v) => Ok(Value::Number(Number::from(v.patch))),
101            Err(_) => Ok(Value::Null),
102        }
103    }
104}
105
106// =============================================================================
107// semver_compare(v1, v2) -> number (-1, 0, 1)
108// =============================================================================
109
110defn!(SemverCompareFn, vec![arg!(string), arg!(string)], None);
111
112impl Function for SemverCompareFn {
113    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
114        self.signature.validate(args, ctx)?;
115        let s1 = args[0].as_str().unwrap();
116        let s2 = args[1].as_str().unwrap();
117
118        let v1 = match Version::parse(s1) {
119            Ok(v) => v,
120            Err(_) => return Ok(Value::Null),
121        };
122        let v2 = match Version::parse(s2) {
123            Ok(v) => v,
124            Err(_) => return Ok(Value::Null),
125        };
126
127        let result = match v1.cmp(&v2) {
128            std::cmp::Ordering::Less => -1,
129            std::cmp::Ordering::Equal => 0,
130            std::cmp::Ordering::Greater => 1,
131        };
132
133        Ok(number_value(result as f64))
134    }
135}
136
137// =============================================================================
138// semver_satisfies(version, requirement) -> bool
139// =============================================================================
140
141defn!(SemverSatisfiesFn, vec![arg!(string), arg!(string)], None);
142
143impl Function for SemverSatisfiesFn {
144    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
145        self.signature.validate(args, ctx)?;
146        let version_str = args[0].as_str().unwrap();
147        let req_str = args[1].as_str().unwrap();
148
149        let version = match Version::parse(version_str) {
150            Ok(v) => v,
151            Err(_) => return Ok(Value::Null),
152        };
153        let req = match VersionReq::parse(req_str) {
154            Ok(r) => r,
155            Err(_) => return Ok(Value::Null),
156        };
157
158        Ok(Value::Bool(req.matches(&version)))
159    }
160}
161
162// =============================================================================
163// semver_is_valid(s) -> bool
164// =============================================================================
165
166defn!(SemverIsValidFn, vec![arg!(string)], None);
167
168impl Function for SemverIsValidFn {
169    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
170        self.signature.validate(args, ctx)?;
171        let s = args[0].as_str().unwrap();
172        let is_valid = Version::parse(s).is_ok();
173        Ok(Value::Bool(is_valid))
174    }
175}
176
177/// Register semver functions filtered by the enabled set.
178pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
179    register_if_enabled(
180        runtime,
181        "semver_parse",
182        enabled,
183        Box::new(SemverParseFn::new()),
184    );
185    register_if_enabled(
186        runtime,
187        "semver_major",
188        enabled,
189        Box::new(SemverMajorFn::new()),
190    );
191    register_if_enabled(
192        runtime,
193        "semver_minor",
194        enabled,
195        Box::new(SemverMinorFn::new()),
196    );
197    register_if_enabled(
198        runtime,
199        "semver_patch",
200        enabled,
201        Box::new(SemverPatchFn::new()),
202    );
203    register_if_enabled(
204        runtime,
205        "semver_compare",
206        enabled,
207        Box::new(SemverCompareFn::new()),
208    );
209    register_if_enabled(
210        runtime,
211        "semver_satisfies",
212        enabled,
213        Box::new(SemverSatisfiesFn::new()),
214    );
215    register_if_enabled(
216        runtime,
217        "semver_is_valid",
218        enabled,
219        Box::new(SemverIsValidFn::new()),
220    );
221}
222
223#[cfg(test)]
224mod tests {
225    use crate::Runtime;
226    use serde_json::json;
227
228    fn setup_runtime() -> Runtime {
229        Runtime::builder()
230            .with_standard()
231            .with_all_extensions()
232            .build()
233    }
234
235    #[test]
236    fn test_semver_parse() {
237        let runtime = setup_runtime();
238        let data = json!("1.2.3");
239        let expr = runtime.compile("semver_parse(@)").unwrap();
240        let result = expr.search(&data).unwrap();
241        let obj = result.as_object().unwrap();
242        assert_eq!(obj.get("major").unwrap().as_f64().unwrap(), 1.0);
243        assert_eq!(obj.get("minor").unwrap().as_f64().unwrap(), 2.0);
244        assert_eq!(obj.get("patch").unwrap().as_f64().unwrap(), 3.0);
245    }
246
247    #[test]
248    fn test_semver_parse_with_pre() {
249        let runtime = setup_runtime();
250        let data = json!("1.0.0-alpha.1");
251        let expr = runtime.compile("semver_parse(@)").unwrap();
252        let result = expr.search(&data).unwrap();
253        let obj = result.as_object().unwrap();
254        assert_eq!(obj.get("major").unwrap().as_f64().unwrap(), 1.0);
255        assert_eq!(obj.get("pre").unwrap().as_str().unwrap(), "alpha.1");
256    }
257
258    #[test]
259    fn test_semver_major() {
260        let runtime = setup_runtime();
261        let data = json!("2.3.4");
262        let expr = runtime.compile("semver_major(@)").unwrap();
263        let result = expr.search(&data).unwrap();
264        assert_eq!(result.as_f64().unwrap(), 2.0);
265    }
266
267    #[test]
268    fn test_semver_minor() {
269        let runtime = setup_runtime();
270        let data = json!("2.3.4");
271        let expr = runtime.compile("semver_minor(@)").unwrap();
272        let result = expr.search(&data).unwrap();
273        assert_eq!(result.as_f64().unwrap(), 3.0);
274    }
275
276    #[test]
277    fn test_semver_patch_fn() {
278        let runtime = setup_runtime();
279        let data = json!("2.3.4");
280        let expr = runtime.compile("semver_patch(@)").unwrap();
281        let result = expr.search(&data).unwrap();
282        assert_eq!(result.as_f64().unwrap(), 4.0);
283    }
284
285    #[test]
286    fn test_semver_compare_less() {
287        let runtime = setup_runtime();
288        let data = json!(["1.0.0", "2.0.0"]);
289        let expr = runtime.compile("semver_compare(@[0], @[1])").unwrap();
290        let result = expr.search(&data).unwrap();
291        assert_eq!(result.as_f64().unwrap(), -1.0);
292    }
293
294    #[test]
295    fn test_semver_compare_equal() {
296        let runtime = setup_runtime();
297        let data = json!(["1.0.0", "1.0.0"]);
298        let expr = runtime.compile("semver_compare(@[0], @[1])").unwrap();
299        let result = expr.search(&data).unwrap();
300        assert_eq!(result.as_f64().unwrap(), 0.0);
301    }
302
303    #[test]
304    fn test_semver_compare_greater() {
305        let runtime = setup_runtime();
306        let data = json!(["2.0.0", "1.0.0"]);
307        let expr = runtime.compile("semver_compare(@[0], @[1])").unwrap();
308        let result = expr.search(&data).unwrap();
309        assert_eq!(result.as_f64().unwrap(), 1.0);
310    }
311
312    #[test]
313    fn test_semver_satisfies_true() {
314        let runtime = setup_runtime();
315        let data = json!(["1.2.3", "^1.0.0"]);
316        let expr = runtime.compile("semver_satisfies(@[0], @[1])").unwrap();
317        let result = expr.search(&data).unwrap();
318        assert_eq!(result, json!(true));
319    }
320
321    #[test]
322    fn test_semver_satisfies_false() {
323        let runtime = setup_runtime();
324        let data = json!(["2.0.0", "^1.0.0"]);
325        let expr = runtime.compile("semver_satisfies(@[0], @[1])").unwrap();
326        let result = expr.search(&data).unwrap();
327        assert_eq!(result, json!(false));
328    }
329
330    #[test]
331    fn test_semver_satisfies_tilde() {
332        let runtime = setup_runtime();
333        let data = json!(["1.2.5", "~1.2.0"]);
334        let expr = runtime.compile("semver_satisfies(@[0], @[1])").unwrap();
335        let result = expr.search(&data).unwrap();
336        assert_eq!(result, json!(true));
337    }
338
339    #[test]
340    fn test_semver_is_valid_true() {
341        let runtime = setup_runtime();
342        let data = json!("1.2.3");
343        let expr = runtime.compile("semver_is_valid(@)").unwrap();
344        let result = expr.search(&data).unwrap();
345        assert_eq!(result, json!(true));
346    }
347
348    #[test]
349    fn test_semver_is_valid_false() {
350        let runtime = setup_runtime();
351        let data = json!("not-a-version");
352        let expr = runtime.compile("semver_is_valid(@)").unwrap();
353        let result = expr.search(&data).unwrap();
354        assert_eq!(result, json!(false));
355    }
356}