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_string().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 time::{DateTime, TimeZone};
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    pub fn timestamp(value: Arc<String>) -> Result<Value> {
344        Ok(Value::Timestamp(_timestamp(value.as_str())?))
345    }
346
347    /// A wrapper around [`parse_duration`] that converts errors into [`ExecutionError`].
348    /// and only returns the duration, rather than returning the remaining input.
349    fn _duration(i: &str) -> Result<crate::duration::Duration> {
350        let (_, duration) = crate::duration::parse_duration(i)
351            .map_err(|e| ExecutionError::function_error("duration", e.to_string()))?;
352        Ok(duration)
353    }
354
355    fn _timestamp(i: &str) -> Result<DateTime> {
356        i.parse::<DateTime>()
357            .map_err(|e| ExecutionError::function_error("timestamp", e.to_string()))
358    }
359
360    pub fn timestamp_year(This(this): This<DateTime>) -> Result<Value> {
361        Ok(this.year().into())
362    }
363
364    pub fn timestamp_month(This(this): This<DateTime>) -> Result<Value> {
365        Ok(((this.month() as i32) - 1).into())
366    }
367
368    pub fn timestamp_year_day(This(this): This<DateTime>) -> Result<Value> {
369        Ok(((this.ordinal() as i32) - 1).into())
370    }
371
372    pub fn timestamp_month_day(This(this): This<DateTime>) -> Result<Value> {
373        Ok(((this.day() as i32) - 1).into())
374    }
375
376    pub fn timestamp_date(This(this): This<DateTime>) -> Result<Value> {
377        Ok((this.day() as i32).into())
378    }
379
380    pub fn timestamp_weekday(This(this): This<DateTime>) -> Result<Value> {
381        Ok((this.weekday().number_from_sunday() as i32).into())
382    }
383
384    pub fn timestamp_hours(This(this): This<DateTime>) -> Result<Value> {
385        Ok((this.hour() as i32).into())
386    }
387
388    pub fn timestamp_minutes(This(this): This<DateTime>) -> Result<Value> {
389        Ok((this.minute() as i32).into())
390    }
391
392    pub fn timestamp_seconds(This(this): This<DateTime>) -> Result<Value> {
393        Ok((this.second() as i32).into())
394    }
395
396    pub fn timestamp_millis(This(this): This<DateTime>) -> Result<Value> {
397        Ok((this.millisecond() as i32).into())
398    }
399
400    pub fn now() -> Result<Value> {
401        Ok(Value::Timestamp(DateTime::now_in(TimeZone::UTC)))
402    }
403
404    pub fn unix(This(this): This<DateTime>) -> Result<Value> {
405        Ok((this.unix()).into())
406    }
407}
408
409pub fn max(Arguments(args): Arguments) -> Result<Value> {
410    // If items is a list of values, then operate on the list
411    let items = if args.len() == 1 {
412        match &args[0] {
413            Value::List(values) => values,
414            _ => return Ok(args[0].clone()),
415        }
416    } else {
417        &args
418    };
419
420    items
421        .iter()
422        .skip(1)
423        .try_fold(items.first().unwrap_or(&Value::Null), |acc, x| match acc.partial_cmp(x) {
424            Some(Ordering::Greater) => Ok(acc),
425            Some(_) => Ok(x),
426            None => Err(ExecutionError::ValuesNotComparable(acc.clone(), x.clone())),
427        })
428        .cloned()
429}
430
431pub fn min(Arguments(args): Arguments) -> Result<Value> {
432    // If items is a list of values, then operate on the list
433    let items = if args.len() == 1 {
434        match &args[0] {
435            Value::List(values) => values,
436            _ => return Ok(args[0].clone()),
437        }
438    } else {
439        &args
440    };
441
442    items
443        .iter()
444        .skip(1)
445        .try_fold(items.first().unwrap_or(&Value::Null), |acc, x| match acc.partial_cmp(x) {
446            Some(Ordering::Less) => Ok(acc),
447            Some(_) => Ok(x),
448            None => Err(ExecutionError::ValuesNotComparable(acc.clone(), x.clone())),
449        })
450        .cloned()
451}
452
453#[cfg(test)]
454mod tests {
455    use crate::{context::Context, tests::test_script};
456
457    fn assert_script(input: &(&str, &str)) {
458        assert_eq!(test_script(input.1, None), Ok(true.into()), "{}", input.0);
459    }
460
461    fn assert_error(input: &(&str, &str, &str)) {
462        assert_eq!(
463            test_script(input.1, None).expect_err("expected error").to_string(),
464            input.2,
465            "{}",
466            input.0
467        );
468    }
469
470    #[test]
471    fn test_length() {
472        [
473            ("length of list", "length([1, 2, 3]) == 3"),
474            ("length of map", r#"length({"a": 1, "b": 2, "c": 3}) == 3"#),
475            ("length of string", r#"length("foo") == 3"#),
476            ("length of bytes", r#"length(b"foo") == 3"#),
477            ("length as a list method", "[1, 2, 3].length() == 3"),
478            ("length as a string method", r#""foobar".length() == 6"#),
479        ]
480        .iter()
481        .for_each(assert_script);
482    }
483
484    #[test]
485    fn test_has() {
486        let tests = vec![
487            ("map has", "has(foo.bar) == true"),
488            ("map not has", "has(foo.baz) == false"),
489        ];
490
491        for (name, script) in tests {
492            let mut ctx = Context::default();
493            ctx.add_variable_from_value("foo", std::collections::HashMap::from([("bar", 1)]));
494            assert_eq!(test_script(script, Some(ctx)), Ok(true.into()), "{name}");
495        }
496    }
497
498    #[test]
499    fn test_map() {
500        [
501            ("map list", "[1, 2, 3].map(x, x * 2) == [2, 4, 6]"),
502            ("map list 2", "[1, 2, 3].map(y, y + 1) == [2, 3, 4]"),
503            ("map list filter", "[1, 2, 3].map(y, y + 1) == [2, 3, 4]"),
504            ("nested map", "[[1, 2], [2, 3]].map(x, x.map(x, x * 2)) == [[2, 4], [4, 6]]"),
505            ("map to list", r#"{"John": "smart"}.map(key, key) == ["John"]"#),
506        ]
507        .iter()
508        .for_each(assert_script);
509    }
510
511    #[test]
512    fn test_filter() {
513        [("filter list", "[1, 2, 3].filter(x, x > 2) == [3]")]
514            .iter()
515            .for_each(assert_script);
516    }
517
518    #[test]
519    fn test_all() {
520        [
521            ("all list #1", "[0, 1, 2].all(x, x >= 0)"),
522            ("all list #2", "[0, 1, 2].all(x, x > 0) == false"),
523            ("all map", "{0: 0, 1:1, 2:2}.all(x, x >= 0) == true"),
524        ]
525        .iter()
526        .for_each(assert_script);
527    }
528
529    #[test]
530    fn test_any() {
531        [
532            ("exist list #1", "[0, 1, 2].any(x, x > 0)"),
533            ("exist list #2", "[0, 1, 2].any(x, x == 3) == false"),
534            ("exist list #3", "[0, 1, 2, 2].any(x, x == 2)"),
535            ("exist map", "{0: 0, 1:1, 2:2}.any(x, x > 0)"),
536        ]
537        .iter()
538        .for_each(assert_script);
539    }
540
541    // #[test]
542    // fn test_exists_one() {
543    //     [
544    //         ("exist list #1", "[0, 1, 2].exists_one(x, x > 0) == false"),
545    //         ("exist list #2", "[0, 1, 2].exists_one(x, x == 0)"),
546    //         ("exist map", "{0: 0, 1:1, 2:2}.exists_one(x, x == 2)"),
547    //     ]
548    //     .iter()
549    //     .for_each(assert_script);
550    // }
551
552    #[test]
553    fn test_max() {
554        [
555            ("max single", "max(1) == 1"),
556            ("max multiple", "max(1, 2, 3) == 3"),
557            ("max negative", "max(-1, 0) == 0"),
558            ("max float", "max(-1.0, 0.0) == 0.0"),
559            ("max list", "max([1, 2, 3]) == 3"),
560            ("max empty list", "max([]) == null"),
561            ("max no args", "max() == null"),
562        ]
563        .iter()
564        .for_each(assert_script);
565    }
566
567    #[test]
568    fn test_min() {
569        [
570            ("min single", "min(1) == 1"),
571            ("min multiple", "min(1, 2, 3) == 1"),
572            ("min negative", "min(-1, 0) == -1"),
573            ("min float", "min(-1.0, 0.0) == -1.0"),
574            ("min float multiple", "min(1.61803, 3.1415, 2.71828, 1.41421) == 1.41421"),
575            ("min list", "min([1, 2, 3]) == 1"),
576            ("min empty list", "min([]) == null"),
577            ("min no args", "min() == null"),
578        ]
579        .iter()
580        .for_each(assert_script);
581    }
582
583    #[test]
584    fn test_starts_with() {
585        [
586            ("starts with true", r#""foobar".starts_with("foo") == true"#),
587            ("starts with false", r#""foobar".starts_with("bar") == false"#),
588        ]
589        .iter()
590        .for_each(assert_script);
591    }
592
593    #[test]
594    fn test_ends_with() {
595        [
596            ("ends with true", r#""foobar".ends_with("bar") == true"#),
597            ("ends with false", r#""foobar".ends_with("foo") == false"#),
598        ]
599        .iter()
600        .for_each(assert_script);
601    }
602
603    #[cfg(feature = "time")]
604    #[test]
605    fn test_timestamp() {
606        [
607            (
608                "comparison",
609                r#"Timestamp("2023-05-29T00:00:00Z") > Timestamp("2023-05-28T00:00:00Z")"#,
610            ),
611            (
612                "comparison",
613                r#"Timestamp("2023-05-29T00:00:00Z") < Timestamp("2023-05-30T00:00:00Z")"#,
614            ),
615            (
616                "subtracting duration",
617                r#"Timestamp("2023-05-29T00:00:00Z") - Duration("24h") == Timestamp("2023-05-28T00:00:00Z")"#,
618            ),
619            (
620                "subtracting date",
621                r#"Timestamp("2023-05-29T00:00:00Z") - Timestamp("2023-05-28T00:00:00Z") == Duration("24h")"#,
622            ),
623            (
624                "adding duration",
625                r#"Timestamp("2023-05-28T00:00:00Z") + Duration("24h") == Timestamp("2023-05-29T00:00:00Z")"#,
626            ),
627            (
628                "timestamp string",
629                r#"String(Timestamp("2023-05-28T00:00:00Z")) == "2023-05-28T00:00:00Z""#,
630            ),
631            ("timestamp year", r#"Timestamp("2023-05-28T00:00:00Z").year() == 2023"#),
632            ("timestamp month", r#"Timestamp("2023-05-28T00:00:00Z").month() == 4"#),
633            (
634                "timestamp getDayOfMonth",
635                r#"Timestamp("2023-05-28T00:00:00Z").getDayOfMonth() == 27"#,
636            ),
637            (
638                "timestamp getDayOfYear",
639                r#"Timestamp("2023-05-28T00:00:00Z").getDayOfYear() == 147"#,
640            ),
641            ("timestamp getDate", r#"Timestamp("2023-05-28T00:00:00Z").getDate() == 28"#),
642            (
643                "timestamp getDayOfWeek",
644                r#"Timestamp("2023-05-28T00:00:00Z").getDayOfWeek() == 0"#,
645            ),
646            ("timestamp getHours", r#"Timestamp("2023-05-28T02:00:00Z").getHours() == 2"#),
647            (
648                "timestamp getMinutes",
649                r#" Timestamp("2023-05-28T00:05:00Z").getMinutes() == 5"#,
650            ),
651            ("timestamp seconds", r#"Timestamp("2023-05-28T00:00:06Z").seconds() == 6"#),
652            (
653                "timestamp milliseconds",
654                r#"Timestamp("2023-05-28T00:00:42.123Z").milliseconds() == 123"#,
655            ),
656        ]
657        .iter()
658        .for_each(assert_script);
659
660        [
661            (
662                "timestamp out of range",
663                r#"Timestamp("0000-01-00T00:00:00Z")"#,
664                "Error executing function 'timestamp': day is not valid for the given month",
665            ),
666            (
667                "timestamp out of range",
668                r#"Timestamp("9999-12-32T23:59:59.999999999Z")"#,
669                "Error executing function 'timestamp': day is not valid for the given month",
670            ),
671            (
672                "timestamp overflow",
673                r#"Timestamp("9999-12-31T23:59:59Z") + Duration("1s")"#,
674                "Overflow from binary operator 'add': Timestamp(9999-12-31T23:59:59Z), Duration(Duration { nanos: 1000000000 })",
675            ),
676            (
677                "timestamp underflow",
678                r#"Timestamp("0001-01-01T00:00:00Z") - Duration("1s")"#,
679                "Overflow from binary operator 'sub': Timestamp(0001-01-01T00:00:00Z), Duration(Duration { nanos: 1000000000 })",
680            ),
681            (
682                "timestamp underflow",
683                r#"Timestamp("0001-01-01T00:00:00Z") + Duration("-1s")"#,
684                "Overflow from binary operator 'add': Timestamp(0001-01-01T00:00:00Z), Duration(Duration { nanos: -1000000000 })",
685            ),
686        ]
687        .iter()
688        .for_each(assert_error)
689    }
690
691    #[cfg(feature = "time")]
692    #[test]
693    fn test_duration() {
694        [
695            ("duration equal 1", r#"Duration("1s") == Duration("1000ms")"#),
696            ("duration equal 2", r#"Duration("1m") == Duration("60s")"#),
697            ("duration equal 3", r#"Duration("1h") == Duration("60m")"#),
698            ("duration comparison 1", r#"Duration("1m") > Duration("1s")"#),
699            ("duration comparison 2", r#"Duration("1m") < Duration("1h")"#),
700            ("duration subtraction", r#"Duration("1h") - Duration("1m") == Duration("59m")"#),
701            ("duration addition", r#"Duration("1h") + Duration("1m") == Duration("1h1m")"#),
702        ]
703        .iter()
704        .for_each(assert_script);
705    }
706
707    #[cfg(feature = "time")]
708    #[test]
709    fn test_timestamp_variable() {
710        let mut context = Context::default();
711        let ts: time::DateTime = "2023-05-29T00:00:00Z".parse().unwrap();
712        context.add_variable("ts", crate::Value::Timestamp(ts)).unwrap();
713
714        let program = crate::Program::compile(r#"ts == Timestamp("2023-05-29T00:00:00Z")"#).unwrap();
715        let result = program.execute(&context).unwrap();
716        assert_eq!(result, true.into());
717    }
718
719    #[cfg(feature = "time")]
720    #[test]
721    fn test_time_string() {
722        [
723            ("duration", r#"String(Duration("1h30m")) == "1h30m0s""#),
724            (
725                "timestamp",
726                r#"String(Timestamp("2023-05-29T00:00:00Z")) == "2023-05-29T00:00:00Z""#,
727            ),
728        ]
729        .iter()
730        .for_each(assert_script);
731    }
732
733    #[test]
734    fn test_contains() {
735        let tests = vec![
736            ("list", "[1, 2, 3].contains(3) == true"),
737            ("map", "{1: true, 2: true, 3: true}.contains(3) == true"),
738            ("string", r#""foobar".contains("bar") == true"#),
739            ("bytes", r#"b"foobar".contains(b"o") == true"#),
740            #[cfg(feature = "ip")]
741            ("ip", r#"Ip("0.0.0.0/0").contains(Ip("127.0.0.1"))"#),
742            #[cfg(feature = "ip")]
743            ("ip does not contain", r#"!Ip("0.0.0.0/32").contains(Ip("127.0.0.1"))"#),
744        ];
745
746        for (name, script) in tests {
747            assert_eq!(test_script(script, None), Ok(true.into()), "{name}");
748        }
749    }
750
751    #[cfg(feature = "regex")]
752    #[test]
753    fn test_matches() {
754        let tests = vec![
755            ("string", r#""foobar".matches(Regex("^[a-zA-Z]*$")) == true"#),
756            (
757                "map",
758                r#"{"1": "abc", "2": "def", "3": "ghi"}.all(key, key.matches(Regex("^[a-zA-Z]*$"))) == false"#,
759            ),
760        ];
761
762        for (name, script) in tests {
763            assert_eq!(test_script(script, None), Ok(true.into()), ".matches failed for '{name}'");
764        }
765    }
766
767    #[cfg(feature = "regex")]
768    #[test]
769    fn test_regex_err() {
770        assert_eq!(
771            test_script(r#""foobar".matches(Regex("(foo")) == true"#, None),
772            Err(crate::ExecutionError::FunctionError {
773                function: "Regex".to_string(),
774                // message: "'(foo' not a valid regex:\nregex parse error:\n    (foo\n    ^\nerror: unclosed group".to_string()
775                message: "regex parse error:\n    (foo\n    ^\nerror: unclosed group".to_string()
776            })
777        );
778    }
779
780    #[test]
781    fn test_string() {
782        [
783            ("String", r#"String("foo") == "foo""#),
784            ("Int", r#"String(10) == "10""#),
785            ("Float", r#"String(10.5) == "10.5""#),
786            ("Bytes", r#"String(b"foo") == "foo""#),
787        ]
788        .iter()
789        .for_each(assert_script);
790    }
791
792    #[test]
793    fn test_bytes() {
794        [
795            ("String", r#"Bytes("abc") == b"abc""#),
796            ("Bytes", r#"Bytes("abc") == b"\x61b\x63""#),
797        ]
798        .iter()
799        .for_each(assert_script);
800    }
801
802    #[test]
803    fn test_float() {
804        [
805            ("String", r#"Float("10") == 10.0"#),
806            ("Int", "Float(10)== 10.0"),
807            ("Float", "Float(10) == 10.0"),
808        ]
809        .iter()
810        .for_each(assert_script);
811    }
812
813    // #[test]
814    // fn test_uint() {
815    //     [
816    //         ("String", r#"Uint("10") == Uint(10)"#),
817    //         ("Float", "Uint(10.5) == Uint(10)"),
818    //     ]
819    //     .iter()
820    //     .for_each(assert_script);
821    // }
822
823    #[test]
824    fn test_int() {
825        [
826            ("String", r#"Int("10") == 10"#),
827            ("Int", "Int(10) == 10"),
828            // ("Uint", "10.uint().int() == 10"),
829            ("Float", "Int(10.5) == 10"),
830        ]
831        .iter()
832        .for_each(assert_script);
833    }
834
835    #[test]
836    fn no_bool_coercion() {
837        [
838            ("String || bool", r#""" || false"#, "No such overload"),
839            ("Int || bool", "1 || false", "No such overload"),
840            // ("UInt || bool", "1u || false", "No such overload"),
841            ("Float || bool", "0.1|| false", "No such overload"),
842            ("List || bool", "[] || false", "No such overload"),
843            ("Map || bool", "{} || false", "No such overload"),
844            ("null || bool", "null || false", "No such overload"),
845        ]
846        .iter()
847        .for_each(assert_error)
848    }
849}