Skip to main content

bel/
functions.rs

1use std::{cmp::Ordering, convert::TryInto, sync::Arc};
2
3use crate::{
4    ExecutionError,
5    context::Context,
6    magic::{Arguments, This},
7    objects::Value,
8    parser::Expression,
9    resolvers::Resolver,
10};
11
12type Result<T> = std::result::Result<T, ExecutionError>;
13
14/// `FunctionContext` is a context object passed to functions when they are called.
15///
16/// It contains references to the target object (if the function is called as
17/// a method), the program context ([`Context`]) which gives functions access
18/// to variables, and the arguments to the function call.
19#[derive(Clone)]
20pub struct FunctionContext<'context> {
21    pub name: Arc<String>,
22    pub this: Option<Value>,
23    pub ptx: &'context Context<'context>,
24    pub args: Vec<Expression>,
25    pub arg_idx: usize,
26}
27
28impl<'context> FunctionContext<'context> {
29    pub fn new(
30        name: Arc<String>,
31        this: Option<Value>,
32        ptx: &'context Context<'context>,
33        args: Vec<Expression>,
34    ) -> Self {
35        Self {
36            name,
37            this,
38            ptx,
39            args,
40            arg_idx: 0,
41        }
42    }
43
44    /// Resolves the given expression using the program's [`Context`].
45    pub fn resolve<R>(&self, resolver: R) -> Result<Value>
46    where
47        R: Resolver,
48    {
49        resolver.resolve(self)
50    }
51
52    /// Returns an execution error for the currently execution function.
53    pub fn error<M: ToString>(&self, message: M) -> ExecutionError {
54        ExecutionError::function_error(self.name.as_str(), message)
55    }
56}
57
58/// Calculates the size of either the target, or the provided args depending on how
59/// the function is called.
60///
61/// If called as a method, the target will be used. If called as a function, the
62/// first argument will be used.
63///
64/// The following [`Value`] variants are supported:
65/// * [`Value::List`]
66/// * [`Value::Map`]
67/// * [`Value::String`]
68/// * [`Value::Bytes`]
69///
70/// # Examples
71/// ```skip
72/// length([1, 2, 3]) == 3
73/// ```
74/// ```skip
75/// 'foobar'.length() == 6
76/// ```
77pub fn length(ftx: &FunctionContext, This(this): This<Value>) -> Result<i64> {
78    let length = match this {
79        Value::List(l) => l.len(),
80        Value::Map(m) => m.map.len(),
81        Value::String(s) => s.len(),
82        Value::Bytes(b) => b.len(),
83        value => return Err(ftx.error(format!("cannot determine the length of {value:?}"))),
84    };
85    Ok(length as i64)
86}
87
88/// Returns true if the target contains the provided argument. The actual behavior
89/// depends mainly on the type of the target.
90///
91/// The following [`Value`] variants are supported:
92/// * [`Value::List`] - Returns true if the list contains the provided value.
93/// * [`Value::Map`] - Returns true if the map contains the provided key.
94/// * [`Value::String`] - Returns true if the string contains the provided substring.
95/// * [`Value::Bytes`] - Returns true if the bytes contain the provided byte.
96///
97/// # Example
98///
99/// ## List
100/// ```cel
101/// [1, 2, 3].contains(1) == true
102/// ```
103///
104/// ## Map
105/// ```cel
106/// {"a": 1, "b": 2, "c": 3}.contains("a") == true
107/// ```
108///
109/// ## String
110/// ```cel
111/// "abc".contains("b") == true
112/// ```
113///
114/// ## Bytes
115/// ```cel
116/// b"abc".contains(b"c") == true
117/// ```
118pub fn contains(This(this): This<Value>, arg: Value) -> Result<Value> {
119    Ok(match this {
120        Value::List(v) => v.contains(&arg),
121        Value::Map(v) => v
122            .map
123            .contains_key(&arg.try_into().map_err(ExecutionError::UnsupportedKeyType)?),
124        Value::String(s) => {
125            if let Value::String(arg) = arg {
126                s.contains(arg.as_str())
127            } else {
128                false
129            }
130        }
131        Value::Bytes(b) => {
132            if let Value::Bytes(arg) = arg {
133                let s = arg.as_slice();
134                b.windows(arg.len()).any(|w| w == s)
135            } else {
136                false
137            }
138        }
139        #[cfg(feature = "ip")]
140        Value::Ip(v) => {
141            if let Value::Ip(arg) = arg {
142                let is_arg_single_ip = match arg {
143                    ipnetwork::IpNetwork::V4(v4) => v4.prefix() == 32,
144                    ipnetwork::IpNetwork::V6(v6) => v6.prefix() == 128,
145                };
146                is_arg_single_ip && v.contains(arg.ip())
147            } else {
148                false
149            }
150        }
151        _ => false,
152    }
153    .into())
154}
155
156// Performs a type conversion on the target. The following conversions are currently
157// supported:
158// * `string` - Returns a copy of the target string.
159// * `timestamp` - Returns the timestamp in RFC3339 format.
160// * `duration` - Returns the duration in a string formatted like "72h3m0.5s".
161// * `int` - Returns the integer value of the target.
162// * `uint` - Returns the unsigned integer value of the target.
163// * `float` - Returns the float value of the target.
164// * `bytes` - Converts bytes to string using from_utf8_lossy.
165pub fn string(ftx: &FunctionContext, value: Value) -> Result<Value> {
166    Ok(match value {
167        Value::String(v) => Value::String(v.clone()),
168        #[cfg(feature = "time")]
169        Value::Timestamp(t) => Value::String(t.to_rfc3339().into()),
170        #[cfg(feature = "time")]
171        Value::Duration(v) => Value::String(crate::duration::format_duration(&v).into()),
172        Value::Int(v) => Value::String(v.to_string().into()),
173        // Value::UInt(v) => Value::String(v.to_string().into()),
174        Value::Float(v) => Value::String(v.to_string().into()),
175        Value::Bytes(v) => Value::String(Arc::new(String::from_utf8_lossy(v.as_slice()).into())),
176        #[cfg(feature = "regex")]
177        Value::Regex(regex) => Value::String(Arc::new(regex.to_string())),
178        #[cfg(feature = "ip")]
179        Value::Ip(ip) => Value::String(Arc::new(ip.to_string())),
180        v => return Err(ftx.error(format!("cannot convert {v:?} to string"))),
181    })
182}
183
184pub fn bytes(value: Arc<String>) -> Result<Value> {
185    Ok(Value::Bytes(value.as_bytes().to_vec().into()))
186}
187
188// Performs a type conversion on the target.
189pub fn float(ftx: &FunctionContext, value: Value) -> Result<Value> {
190    Ok(match value {
191        Value::String(v) => v
192            .parse::<f64>()
193            .map(Value::Float)
194            .map_err(|e| ftx.error(format!("string parse error: {e}")))?,
195        Value::Float(v) => Value::Float(v),
196        Value::Int(v) => Value::Float(v as f64),
197        // Value::UInt(v) => Value::Float(v as f64),
198        v => return Err(ftx.error(format!("cannot convert {v:?} to Float"))),
199    })
200}
201
202// Performs a type conversion on the target.
203// pub fn uint(ftx: &FunctionContext, value: Value) -> Result<Value> {
204//     Ok(match value {
205//         Value::String(v) => v
206//             .parse::<u64>()
207//             .map(Value::UInt)
208//             .map_err(|e| ftx.error(format!("string parse error: {e}")))?,
209//         Value::Float(v) => {
210//             if v > u64::MAX as f64 || v < u64::MIN as f64 {
211//                 return Err(ftx.error("unsigned integer overflow"));
212//             }
213//             Value::UInt(v as u64)
214//         }
215//         Value::Int(v) => Value::UInt(
216//             v.try_into()
217//                 .map_err(|_| ftx.error("unsigned integer overflow"))?,
218//         ),
219//         Value::UInt(v) => Value::UInt(v),
220//         v => return Err(ftx.error(format!("cannot convert {v:?} to uint"))),
221//     })
222// }
223
224// Performs a type conversion on the target.
225pub fn int(ftx: &FunctionContext, value: Value) -> Result<Value> {
226    Ok(match value {
227        Value::String(v) => v
228            .parse::<i64>()
229            .map(Value::Int)
230            .map_err(|e| ftx.error(format!("string parse error: {e}")))?,
231        Value::Float(v) => {
232            if v > i64::MAX as f64 || v < i64::MIN as f64 {
233                return Err(ftx.error("integer overflow"));
234            }
235            Value::Int(v as i64)
236        }
237        Value::Int(v) => Value::Int(v),
238        // Value::UInt(v) => Value::Int(v.try_into().map_err(|_| ftx.error("integer overflow"))?),
239        v => return Err(ftx.error(format!("cannot convert {v:?} to int"))),
240    })
241}
242
243/// Returns true if a string starts with another string.
244///
245/// # Example
246/// ```cel
247/// "abc".starts_with("a") == true
248/// ```
249pub fn starts_with(This(this): This<Arc<String>>, prefix: Arc<String>) -> bool {
250    this.starts_with(prefix.as_str())
251}
252
253/// Returns true if a string ends with another string.
254///
255/// # Example
256/// ```cel
257/// "abc".ends_with("c") == true
258/// ```
259pub fn ends_with(This(this): This<Arc<String>>, suffix: Arc<String>) -> bool {
260    this.ends_with(suffix.as_str())
261}
262
263/// Returns true if a string matches the regular expression.
264///
265/// # Example
266/// ```cel
267/// "abc".matches("^[a-z]*$") == true
268/// ```
269#[cfg(feature = "regex")]
270pub fn matches(
271    // ftx: &FunctionContext,
272    This(this): This<Arc<String>>,
273    regex: regex::Regex,
274) -> bool {
275    regex.is_match(&this)
276    // match &regex {
277    //     Value::Regex(regex) => Ok(regex.is_match(&this)),
278    //     _ => Err(ftx.error(format!("matches mut be used with a regex"))),
279    // }
280    // match regex::Regex::new(&regex) {
281    //     Ok(re) => Ok(re.is_match(&this)),
282    //     Err(err) => Err(ftx.error(format!("'{regex}' not a valid regex:\n{err}"))),
283    // }
284}
285
286#[cfg(feature = "regex")]
287pub fn regex(ftx: &FunctionContext, This(this): This<Value>) -> Result<Value> {
288    Ok(match this {
289        Value::String(v) => Value::Regex(regex::Regex::new(v.as_str()).map_err(|e| ftx.error(e.to_string()))?),
290        v => return Err(ftx.error(format!("cannot convert {v:?} to Regex"))),
291    })
292}
293
294#[cfg(feature = "time")]
295pub use time::duration;
296#[cfg(feature = "time")]
297pub use time::timestamp;
298
299// Performs a type conversion on the target.
300#[cfg(feature = "ip")]
301pub fn ip(ftx: &FunctionContext, value: Value) -> Result<Value> {
302    match value {
303        Value::String(v) => {
304            use ipnetwork::IpNetwork;
305            let ip: IpNetwork = v
306                .parse()
307                .map_err(|err| ftx.error(format!("error converting {v:?} to Ip: {err}")))?;
308            Ok(Value::Ip(ip))
309        }
310        v => Err(ftx.error(format!("cannot convert {v:?} to String"))),
311    }
312}
313
314#[cfg(feature = "time")]
315pub mod time {
316    use std::sync::Arc;
317
318    use chrono::{Datelike, Days, Months, Timelike, Utc};
319
320    use super::Result;
321    use crate::{ExecutionError, Value, magic::This};
322
323    /// Duration parses the provided argument into a [`Value::Duration`] value.
324    ///
325    /// The argument must be string, and must be in the format of a duration. See
326    /// the [`parse_duration`] documentation for more information on the supported
327    /// formats.
328    ///
329    /// # Examples
330    /// - `1h` parses as 1 hour
331    /// - `1.5h` parses as 1 hour and 30 minutes
332    /// - `1h30m` parses as 1 hour and 30 minutes
333    /// - `1h30m1s` parses as 1 hour, 30 minutes, and 1 second
334    /// - `1ms` parses as 1 millisecond
335    /// - `1.5ms` parses as 1 millisecond and 500 microseconds
336    /// - `1ns` parses as 1 nanosecond
337    /// - `1.5ns` parses as 1 nanosecond (sub-nanosecond durations not supported)
338    pub fn duration(value: Arc<String>) -> crate::functions::Result<Value> {
339        Ok(Value::Duration(_duration(value.as_str())?))
340    }
341
342    /// Timestamp parses the provided argument into a [`Value::Timestamp`] value.
343    /// The
344    pub fn timestamp(value: Arc<String>) -> Result<Value> {
345        Ok(Value::Timestamp(chrono::DateTime::parse_from_rfc3339(value.as_str()).map_err(
346            |e| ExecutionError::function_error("timestamp", e.to_string().as_str()),
347        )?))
348    }
349
350    /// A wrapper around [`parse_duration`] that converts errors into [`ExecutionError`].
351    /// and only returns the duration, rather than returning the remaining input.
352    fn _duration(i: &str) -> Result<chrono::Duration> {
353        let (_, duration) = crate::duration::parse_duration(i)
354            .map_err(|e| ExecutionError::function_error("duration", e.to_string()))?;
355        Ok(duration)
356    }
357
358    fn _timestamp(i: &str) -> Result<chrono::DateTime<chrono::FixedOffset>> {
359        chrono::DateTime::parse_from_rfc3339(i).map_err(|e| ExecutionError::function_error("timestamp", e.to_string()))
360    }
361
362    pub fn timestamp_year(This(this): This<chrono::DateTime<chrono::FixedOffset>>) -> Result<Value> {
363        Ok(this.year().into())
364    }
365
366    pub fn timestamp_month(This(this): This<chrono::DateTime<chrono::FixedOffset>>) -> Result<Value> {
367        Ok((this.month0() as i32).into())
368    }
369
370    pub fn timestamp_year_day(This(this): This<chrono::DateTime<chrono::FixedOffset>>) -> Result<Value> {
371        let year = this
372            .checked_sub_days(Days::new(this.day0() as u64))
373            .unwrap()
374            .checked_sub_months(Months::new(this.month0()))
375            .unwrap();
376        Ok(this.signed_duration_since(year).num_days().into())
377    }
378
379    pub fn timestamp_month_day(This(this): This<chrono::DateTime<chrono::FixedOffset>>) -> Result<Value> {
380        Ok((this.day0() as i32).into())
381    }
382
383    pub fn timestamp_date(This(this): This<chrono::DateTime<chrono::FixedOffset>>) -> Result<Value> {
384        Ok((this.day() as i32).into())
385    }
386
387    pub fn timestamp_weekday(This(this): This<chrono::DateTime<chrono::FixedOffset>>) -> Result<Value> {
388        Ok((this.weekday().num_days_from_sunday() as i32).into())
389    }
390
391    pub fn timestamp_hours(This(this): This<chrono::DateTime<chrono::FixedOffset>>) -> Result<Value> {
392        Ok((this.hour() as i32).into())
393    }
394
395    pub fn timestamp_minutes(This(this): This<chrono::DateTime<chrono::FixedOffset>>) -> Result<Value> {
396        Ok((this.minute() as i32).into())
397    }
398
399    pub fn timestamp_seconds(This(this): This<chrono::DateTime<chrono::FixedOffset>>) -> Result<Value> {
400        Ok((this.second() as i32).into())
401    }
402
403    pub fn timestamp_millis(This(this): This<chrono::DateTime<chrono::FixedOffset>>) -> Result<Value> {
404        Ok((this.timestamp_subsec_millis() as i32).into())
405    }
406
407    pub fn now() -> Result<Value> {
408        Ok(Value::Timestamp(Utc::now().fixed_offset()))
409    }
410
411    pub fn unix(This(this): This<chrono::DateTime<chrono::FixedOffset>>) -> Result<Value> {
412        Ok((this.timestamp()).into())
413    }
414}
415
416pub fn max(Arguments(args): Arguments) -> Result<Value> {
417    // If items is a list of values, then operate on the list
418    let items = if args.len() == 1 {
419        match &args[0] {
420            Value::List(values) => values,
421            _ => return Ok(args[0].clone()),
422        }
423    } else {
424        &args
425    };
426
427    items
428        .iter()
429        .skip(1)
430        .try_fold(items.first().unwrap_or(&Value::Null), |acc, x| match acc.partial_cmp(x) {
431            Some(Ordering::Greater) => Ok(acc),
432            Some(_) => Ok(x),
433            None => Err(ExecutionError::ValuesNotComparable(acc.clone(), x.clone())),
434        })
435        .cloned()
436}
437
438pub fn min(Arguments(args): Arguments) -> Result<Value> {
439    // If items is a list of values, then operate on the list
440    let items = if args.len() == 1 {
441        match &args[0] {
442            Value::List(values) => values,
443            _ => return Ok(args[0].clone()),
444        }
445    } else {
446        &args
447    };
448
449    items
450        .iter()
451        .skip(1)
452        .try_fold(items.first().unwrap_or(&Value::Null), |acc, x| match acc.partial_cmp(x) {
453            Some(Ordering::Less) => Ok(acc),
454            Some(_) => Ok(x),
455            None => Err(ExecutionError::ValuesNotComparable(acc.clone(), x.clone())),
456        })
457        .cloned()
458}
459
460#[cfg(test)]
461mod tests {
462    use crate::{context::Context, tests::test_script};
463
464    fn assert_script(input: &(&str, &str)) {
465        assert_eq!(test_script(input.1, None), Ok(true.into()), "{}", input.0);
466    }
467
468    fn assert_error(input: &(&str, &str, &str)) {
469        assert_eq!(
470            test_script(input.1, None).expect_err("expected error").to_string(),
471            input.2,
472            "{}",
473            input.0
474        );
475    }
476
477    #[test]
478    fn test_length() {
479        [
480            ("length of list", "length([1, 2, 3]) == 3"),
481            ("length of map", r#"length({"a": 1, "b": 2, "c": 3}) == 3"#),
482            ("length of string", r#"length("foo") == 3"#),
483            ("length of bytes", r#"length(b"foo") == 3"#),
484            ("length as a list method", "[1, 2, 3].length() == 3"),
485            ("length as a string method", r#""foobar".length() == 6"#),
486        ]
487        .iter()
488        .for_each(assert_script);
489    }
490
491    #[test]
492    fn test_has() {
493        let tests = vec![
494            ("map has", "has(foo.bar) == true"),
495            ("map not has", "has(foo.baz) == false"),
496        ];
497
498        for (name, script) in tests {
499            let mut ctx = Context::default();
500            ctx.add_variable_from_value("foo", std::collections::HashMap::from([("bar", 1)]));
501            assert_eq!(test_script(script, Some(ctx)), Ok(true.into()), "{name}");
502        }
503    }
504
505    #[test]
506    fn test_map() {
507        [
508            ("map list", "[1, 2, 3].map(x, x * 2) == [2, 4, 6]"),
509            ("map list 2", "[1, 2, 3].map(y, y + 1) == [2, 3, 4]"),
510            ("map list filter", "[1, 2, 3].map(y, y + 1) == [2, 3, 4]"),
511            ("nested map", "[[1, 2], [2, 3]].map(x, x.map(x, x * 2)) == [[2, 4], [4, 6]]"),
512            ("map to list", r#"{"John": "smart"}.map(key, key) == ["John"]"#),
513        ]
514        .iter()
515        .for_each(assert_script);
516    }
517
518    #[test]
519    fn test_filter() {
520        [("filter list", "[1, 2, 3].filter(x, x > 2) == [3]")]
521            .iter()
522            .for_each(assert_script);
523    }
524
525    #[test]
526    fn test_all() {
527        [
528            ("all list #1", "[0, 1, 2].all(x, x >= 0)"),
529            ("all list #2", "[0, 1, 2].all(x, x > 0) == false"),
530            ("all map", "{0: 0, 1:1, 2:2}.all(x, x >= 0) == true"),
531        ]
532        .iter()
533        .for_each(assert_script);
534    }
535
536    #[test]
537    fn test_any() {
538        [
539            ("exist list #1", "[0, 1, 2].any(x, x > 0)"),
540            ("exist list #2", "[0, 1, 2].any(x, x == 3) == false"),
541            ("exist list #3", "[0, 1, 2, 2].any(x, x == 2)"),
542            ("exist map", "{0: 0, 1:1, 2:2}.any(x, x > 0)"),
543        ]
544        .iter()
545        .for_each(assert_script);
546    }
547
548    // #[test]
549    // fn test_exists_one() {
550    //     [
551    //         ("exist list #1", "[0, 1, 2].exists_one(x, x > 0) == false"),
552    //         ("exist list #2", "[0, 1, 2].exists_one(x, x == 0)"),
553    //         ("exist map", "{0: 0, 1:1, 2:2}.exists_one(x, x == 2)"),
554    //     ]
555    //     .iter()
556    //     .for_each(assert_script);
557    // }
558
559    #[test]
560    fn test_max() {
561        [
562            ("max single", "max(1) == 1"),
563            ("max multiple", "max(1, 2, 3) == 3"),
564            ("max negative", "max(-1, 0) == 0"),
565            ("max float", "max(-1.0, 0.0) == 0.0"),
566            ("max list", "max([1, 2, 3]) == 3"),
567            ("max empty list", "max([]) == null"),
568            ("max no args", "max() == null"),
569        ]
570        .iter()
571        .for_each(assert_script);
572    }
573
574    #[test]
575    fn test_min() {
576        [
577            ("min single", "min(1) == 1"),
578            ("min multiple", "min(1, 2, 3) == 1"),
579            ("min negative", "min(-1, 0) == -1"),
580            ("min float", "min(-1.0, 0.0) == -1.0"),
581            ("min float multiple", "min(1.61803, 3.1415, 2.71828, 1.41421) == 1.41421"),
582            ("min list", "min([1, 2, 3]) == 1"),
583            ("min empty list", "min([]) == null"),
584            ("min no args", "min() == null"),
585        ]
586        .iter()
587        .for_each(assert_script);
588    }
589
590    #[test]
591    fn test_starts_with() {
592        [
593            ("starts with true", r#""foobar".starts_with("foo") == true"#),
594            ("starts with false", r#""foobar".starts_with("bar") == false"#),
595        ]
596        .iter()
597        .for_each(assert_script);
598    }
599
600    #[test]
601    fn test_ends_with() {
602        [
603            ("ends with true", r#""foobar".ends_with("bar") == true"#),
604            ("ends with false", r#""foobar".ends_with("foo") == false"#),
605        ]
606        .iter()
607        .for_each(assert_script);
608    }
609
610    #[cfg(feature = "time")]
611    #[test]
612    fn test_timestamp() {
613        [
614            (
615                "comparison",
616                r#"Timestamp("2023-05-29T00:00:00Z") > Timestamp("2023-05-28T00:00:00Z")"#,
617            ),
618            (
619                "comparison",
620                r#"Timestamp("2023-05-29T00:00:00Z") < Timestamp("2023-05-30T00:00:00Z")"#,
621            ),
622            (
623                "subtracting duration",
624                r#"Timestamp("2023-05-29T00:00:00Z") - Duration("24h") == Timestamp("2023-05-28T00:00:00Z")"#,
625            ),
626            (
627                "subtracting date",
628                r#"Timestamp("2023-05-29T00:00:00Z") - Timestamp("2023-05-28T00:00:00Z") == Duration("24h")"#,
629            ),
630            (
631                "adding duration",
632                r#"Timestamp("2023-05-28T00:00:00Z") + Duration("24h") == Timestamp("2023-05-29T00:00:00Z")"#,
633            ),
634            (
635                "timestamp string",
636                r#"String(Timestamp("2023-05-28T00:00:00Z")) == "2023-05-28T00:00:00+00:00""#,
637            ),
638            ("timestamp year", r#"Timestamp("2023-05-28T00:00:00Z").year() == 2023"#),
639            ("timestamp month", r#"Timestamp("2023-05-28T00:00:00Z").month() == 4"#),
640            (
641                "timestamp getDayOfMonth",
642                r#"Timestamp("2023-05-28T00:00:00Z").getDayOfMonth() == 27"#,
643            ),
644            (
645                "timestamp getDayOfYear",
646                r#"Timestamp("2023-05-28T00:00:00Z").getDayOfYear() == 147"#,
647            ),
648            ("timestamp getDate", r#"Timestamp("2023-05-28T00:00:00Z").getDate() == 28"#),
649            (
650                "timestamp getDayOfWeek",
651                r#"Timestamp("2023-05-28T00:00:00Z").getDayOfWeek() == 0"#,
652            ),
653            ("timestamp getHours", r#"Timestamp("2023-05-28T02:00:00Z").getHours() == 2"#),
654            (
655                "timestamp getMinutes",
656                r#" Timestamp("2023-05-28T00:05:00Z").getMinutes() == 5"#,
657            ),
658            ("timestamp seconds", r#"Timestamp("2023-05-28T00:00:06Z").seconds() == 6"#),
659            (
660                "timestamp milliseconds",
661                r#"Timestamp("2023-05-28T00:00:42.123Z").milliseconds() == 123"#,
662            ),
663        ]
664        .iter()
665        .for_each(assert_script);
666
667        [
668            (
669                "timestamp out of range",
670                r#"Timestamp("0000-01-00T00:00:00Z")"#,
671                "Error executing function 'timestamp': input is out of range",
672            ),
673            (
674                "timestamp out of range",
675                r#"Timestamp("9999-12-32T23:59:59.999999999Z")"#,
676                "Error executing function 'timestamp': input is out of range",
677            ),
678            (
679                "timestamp overflow",
680                r#"Timestamp("9999-12-31T23:59:59Z") + Duration("1s")"#,
681                "Overflow from binary operator 'add': Timestamp(9999-12-31T23:59:59+00:00), Duration(TimeDelta { secs: 1, nanos: 0 })",
682            ),
683            (
684                "timestamp underflow",
685                r#"Timestamp("0001-01-01T00:00:00Z") - Duration("1s")"#,
686                "Overflow from binary operator 'sub': Timestamp(0001-01-01T00:00:00+00:00), Duration(TimeDelta { secs: 1, nanos: 0 })",
687            ),
688            (
689                "timestamp underflow",
690                r#"Timestamp("0001-01-01T00:00:00Z") + Duration("-1s")"#,
691                "Overflow from binary operator 'add': Timestamp(0001-01-01T00:00:00+00:00), Duration(TimeDelta { secs: -1, nanos: 0 })",
692            ),
693        ]
694        .iter()
695        .for_each(assert_error)
696    }
697
698    #[cfg(feature = "time")]
699    #[test]
700    fn test_duration() {
701        [
702            ("duration equal 1", r#"Duration("1s") == Duration("1000ms")"#),
703            ("duration equal 2", r#"Duration("1m") == Duration("60s")"#),
704            ("duration equal 3", r#"Duration("1h") == Duration("60m")"#),
705            ("duration comparison 1", r#"Duration("1m") > Duration("1s")"#),
706            ("duration comparison 2", r#"Duration("1m") < Duration("1h")"#),
707            ("duration subtraction", r#"Duration("1h") - Duration("1m") == Duration("59m")"#),
708            ("duration addition", r#"Duration("1h") + Duration("1m") == Duration("1h1m")"#),
709        ]
710        .iter()
711        .for_each(assert_script);
712    }
713
714    #[cfg(feature = "time")]
715    #[test]
716    fn test_timestamp_variable() {
717        let mut context = Context::default();
718        let ts: chrono::DateTime<chrono::FixedOffset> =
719            chrono::DateTime::parse_from_rfc3339("2023-05-29T00:00:00Z").unwrap();
720        context.add_variable("ts", crate::Value::Timestamp(ts)).unwrap();
721
722        let program = crate::Program::compile(r#"ts == Timestamp("2023-05-29T00:00:00Z")"#).unwrap();
723        let result = program.execute(&context).unwrap();
724        assert_eq!(result, true.into());
725    }
726
727    #[cfg(feature = "time")]
728    #[test]
729    fn test_chrono_string() {
730        [
731            ("duration", r#"String(Duration("1h30m")) == "1h30m0s""#),
732            (
733                "timestamp",
734                r#"String(Timestamp("2023-05-29T00:00:00Z")) == "2023-05-29T00:00:00+00:00""#,
735            ),
736        ]
737        .iter()
738        .for_each(assert_script);
739    }
740
741    #[test]
742    fn test_contains() {
743        let tests = vec![
744            ("list", "[1, 2, 3].contains(3) == true"),
745            ("map", "{1: true, 2: true, 3: true}.contains(3) == true"),
746            ("string", r#""foobar".contains("bar") == true"#),
747            ("bytes", r#"b"foobar".contains(b"o") == true"#),
748            #[cfg(feature = "ip")]
749            ("ip", r#"Ip("0.0.0.0/0").contains(Ip("127.0.0.1"))"#),
750            #[cfg(feature = "ip")]
751            ("ip does not contain", r#"!Ip("0.0.0.0/32").contains(Ip("127.0.0.1"))"#),
752        ];
753
754        for (name, script) in tests {
755            assert_eq!(test_script(script, None), Ok(true.into()), "{name}");
756        }
757    }
758
759    #[cfg(feature = "regex")]
760    #[test]
761    fn test_matches() {
762        let tests = vec![
763            ("string", r#""foobar".matches(Regex("^[a-zA-Z]*$")) == true"#),
764            (
765                "map",
766                r#"{"1": "abc", "2": "def", "3": "ghi"}.all(key, key.matches(Regex("^[a-zA-Z]*$"))) == false"#,
767            ),
768        ];
769
770        for (name, script) in tests {
771            assert_eq!(test_script(script, None), Ok(true.into()), ".matches failed for '{name}'");
772        }
773    }
774
775    #[cfg(feature = "regex")]
776    #[test]
777    fn test_regex_err() {
778        assert_eq!(
779            test_script(r#""foobar".matches(Regex("(foo")) == true"#, None),
780            Err(crate::ExecutionError::FunctionError {
781                function: "Regex".to_string(),
782                // message: "'(foo' not a valid regex:\nregex parse error:\n    (foo\n    ^\nerror: unclosed group".to_string()
783                message: "regex parse error:\n    (foo\n    ^\nerror: unclosed group".to_string()
784            })
785        );
786    }
787
788    #[test]
789    fn test_string() {
790        [
791            ("String", r#"String("foo") == "foo""#),
792            ("Int", r#"String(10) == "10""#),
793            ("Float", r#"String(10.5) == "10.5""#),
794            ("Bytes", r#"String(b"foo") == "foo""#),
795        ]
796        .iter()
797        .for_each(assert_script);
798    }
799
800    #[test]
801    fn test_bytes() {
802        [
803            ("String", r#"Bytes("abc") == b"abc""#),
804            ("Bytes", r#"Bytes("abc") == b"\x61b\x63""#),
805        ]
806        .iter()
807        .for_each(assert_script);
808    }
809
810    #[test]
811    fn test_float() {
812        [
813            ("String", r#"Float("10") == 10.0"#),
814            ("Int", "Float(10)== 10.0"),
815            ("Float", "Float(10) == 10.0"),
816        ]
817        .iter()
818        .for_each(assert_script);
819    }
820
821    // #[test]
822    // fn test_uint() {
823    //     [
824    //         ("String", r#"Uint("10") == Uint(10)"#),
825    //         ("Float", "Uint(10.5) == Uint(10)"),
826    //     ]
827    //     .iter()
828    //     .for_each(assert_script);
829    // }
830
831    #[test]
832    fn test_int() {
833        [
834            ("String", r#"Int("10") == 10"#),
835            ("Int", "Int(10) == 10"),
836            // ("Uint", "10.uint().int() == 10"),
837            ("Float", "Int(10.5) == 10"),
838        ]
839        .iter()
840        .for_each(assert_script);
841    }
842
843    #[test]
844    fn no_bool_coercion() {
845        [
846            ("String || bool", r#""" || false"#, "No such overload"),
847            ("Int || bool", "1 || false", "No such overload"),
848            // ("UInt || bool", "1u || false", "No such overload"),
849            ("Float || bool", "0.1|| false", "No such overload"),
850            ("List || bool", "[] || false", "No such overload"),
851            ("Map || bool", "{} || false", "No such overload"),
852            ("null || bool", "null || false", "No such overload"),
853        ]
854        .iter()
855        .for_each(assert_error)
856    }
857}