cel_interpreter/
functions.rs

1use crate::context::Context;
2use crate::magic::{Arguments, Identifier, This};
3use crate::objects::{Value, ValueType};
4use crate::resolvers::{Argument, Resolver};
5use crate::ExecutionError;
6use cel_parser::Expression;
7use std::cmp::Ordering;
8use std::convert::TryInto;
9use std::sync::Arc;
10
11type Result<T> = std::result::Result<T, ExecutionError>;
12
13/// `FunctionContext` is a context object passed to functions when they are called.
14///
15/// It contains references to the target object (if the function is called as
16/// a method), the program context ([`Context`]) which gives functions access
17/// to variables, and the arguments to the function call.
18#[derive(Clone)]
19pub struct FunctionContext<'context> {
20    pub name: Arc<String>,
21    pub this: Option<Value>,
22    pub ptx: &'context Context<'context>,
23    pub args: Vec<Expression>,
24    pub arg_idx: usize,
25}
26
27impl<'context> FunctionContext<'context> {
28    pub fn new(
29        name: Arc<String>,
30        this: Option<Value>,
31        ptx: &'context Context<'context>,
32        args: Vec<Expression>,
33    ) -> Self {
34        Self {
35            name,
36            this,
37            ptx,
38            args,
39            arg_idx: 0,
40        }
41    }
42
43    /// Resolves the given expression using the program's [`Context`].
44    pub fn resolve<R>(&self, resolver: R) -> Result<Value>
45    where
46        R: Resolver,
47    {
48        resolver.resolve(self)
49    }
50
51    /// Returns an execution error for the currently execution function.
52    pub fn error<M: ToString>(&self, message: M) -> ExecutionError {
53        ExecutionError::function_error(self.name.as_str(), message)
54    }
55}
56
57/// Calculates the size of either the target, or the provided args depending on how
58/// the function is called.
59///
60/// If called as a method, the target will be used. If called as a function, the
61/// first argument will be used.
62///
63/// The following [`Value`] variants are supported:
64/// * [`Value::List`]
65/// * [`Value::Map`]
66/// * [`Value::String`]
67/// * [`Value::Bytes`]
68///
69/// # Examples
70/// ```skip
71/// size([1, 2, 3]) == 3
72/// ```
73/// ```skip
74/// 'foobar'.size() == 6
75/// ```
76pub fn size(ftx: &FunctionContext, This(this): This<Value>) -> Result<i64> {
77    let size = match this {
78        Value::List(l) => l.len(),
79        Value::Map(m) => m.map.len(),
80        Value::String(s) => s.len(),
81        Value::Bytes(b) => b.len(),
82        value => return Err(ftx.error(format!("cannot determine the size of {:?}", value))),
83    };
84    Ok(size as i64)
85}
86
87/// Returns true if the target contains the provided argument. The actual behavior
88/// depends mainly on the type of the target.
89///
90/// The following [`Value`] variants are supported:
91/// * [`Value::List`] - Returns true if the list contains the provided value.
92/// * [`Value::Map`] - Returns true if the map contains the provided key.
93/// * [`Value::String`] - Returns true if the string contains the provided substring.
94/// * [`Value::Bytes`] - Returns true if the bytes contain the provided byte.
95///
96/// # Example
97///
98/// ## List
99/// ```cel
100/// [1, 2, 3].contains(1) == true
101/// ```
102///
103/// ## Map
104/// ```cel
105/// {"a": 1, "b": 2, "c": 3}.contains("a") == true
106/// ```
107///
108/// ## String
109/// ```cel
110/// "abc".contains("b") == true
111/// ```
112///
113/// ## Bytes
114/// ```cel
115/// b"abc".contains(b"c") == true
116/// ```
117pub fn contains(This(this): This<Value>, arg: Value) -> Result<Value> {
118    Ok(match this {
119        Value::List(v) => v.contains(&arg),
120        Value::Map(v) => v
121            .map
122            .contains_key(&arg.try_into().map_err(ExecutionError::UnsupportedKeyType)?),
123        Value::String(s) => {
124            if let Value::String(arg) = arg {
125                s.contains(arg.as_str())
126            } else {
127                false
128            }
129        }
130        Value::Bytes(b) => {
131            if let Value::Bytes(arg) = arg {
132                let s = arg.as_slice();
133                b.windows(arg.len()).any(|w| w == s)
134            } else {
135                false
136            }
137        }
138        _ => false,
139    }
140    .into())
141}
142
143// Performs a type conversion on the target. The following conversions are currently
144// supported:
145// * `string` - Returns a copy of the target string.
146// * `timestamp` - Returns the timestamp in RFC3339 format.
147// * `duration` - Returns the duration in a string formatted like "72h3m0.5s".
148// * `int` - Returns the integer value of the target.
149// * `uint` - Returns the unsigned integer value of the target.
150// * `float` - Returns the float value of the target.
151// * `bytes` - Converts bytes to string using from_utf8_lossy.
152pub fn string(ftx: &FunctionContext, This(this): This<Value>) -> Result<Value> {
153    Ok(match this {
154        Value::String(v) => Value::String(v.clone()),
155        #[cfg(feature = "chrono")]
156        Value::Timestamp(t) => Value::String(t.to_rfc3339().into()),
157        #[cfg(feature = "chrono")]
158        Value::Duration(v) => Value::String(crate::duration::format_duration(&v).into()),
159        Value::Int(v) => Value::String(v.to_string().into()),
160        Value::UInt(v) => Value::String(v.to_string().into()),
161        Value::Float(v) => Value::String(v.to_string().into()),
162        Value::Bytes(v) => Value::String(Arc::new(String::from_utf8_lossy(v.as_slice()).into())),
163        v => return Err(ftx.error(format!("cannot convert {:?} to string", v))),
164    })
165}
166
167pub fn bytes(value: Arc<String>) -> Result<Value> {
168    Ok(Value::Bytes(value.as_bytes().to_vec().into()))
169}
170
171// Performs a type conversion on the target.
172pub fn double(ftx: &FunctionContext, This(this): This<Value>) -> Result<Value> {
173    Ok(match this {
174        Value::String(v) => v
175            .parse::<f64>()
176            .map(Value::Float)
177            .map_err(|e| ftx.error(format!("string parse error: {e}")))?,
178        Value::Float(v) => Value::Float(v),
179        Value::Int(v) => Value::Float(v as f64),
180        Value::UInt(v) => Value::Float(v as f64),
181        v => return Err(ftx.error(format!("cannot convert {:?} to double", v))),
182    })
183}
184
185// Performs a type conversion on the target.
186pub fn uint(ftx: &FunctionContext, This(this): This<Value>) -> Result<Value> {
187    Ok(match this {
188        Value::String(v) => v
189            .parse::<u64>()
190            .map(Value::UInt)
191            .map_err(|e| ftx.error(format!("string parse error: {e}")))?,
192        Value::Float(v) => {
193            if v > u64::MAX as f64 || v < u64::MIN as f64 {
194                return Err(ftx.error("unsigned integer overflow"));
195            }
196            Value::UInt(v as u64)
197        }
198        Value::Int(v) => Value::UInt(
199            v.try_into()
200                .map_err(|_| ftx.error("unsigned integer overflow"))?,
201        ),
202        Value::UInt(v) => Value::UInt(v),
203        v => return Err(ftx.error(format!("cannot convert {:?} to uint", v))),
204    })
205}
206
207// Performs a type conversion on the target.
208pub fn int(ftx: &FunctionContext, This(this): This<Value>) -> Result<Value> {
209    Ok(match this {
210        Value::String(v) => v
211            .parse::<i64>()
212            .map(Value::Int)
213            .map_err(|e| ftx.error(format!("string parse error: {e}")))?,
214        Value::Float(v) => {
215            if v > i64::MAX as f64 || v < i64::MIN as f64 {
216                return Err(ftx.error("integer overflow"));
217            }
218            Value::Int(v as i64)
219        }
220        Value::Int(v) => Value::Int(v),
221        Value::UInt(v) => Value::Int(v.try_into().map_err(|_| ftx.error("integer overflow"))?),
222        v => return Err(ftx.error(format!("cannot convert {:?} to int", v))),
223    })
224}
225
226/// Returns true if a string starts with another string.
227///
228/// # Example
229/// ```cel
230/// "abc".startsWith("a") == true
231/// ```
232pub fn starts_with(This(this): This<Arc<String>>, prefix: Arc<String>) -> bool {
233    this.starts_with(prefix.as_str())
234}
235
236/// Returns true if a string ends with another string.
237///
238/// # Example
239/// ```cel
240/// "abc".endsWith("c") == true
241/// ```
242pub fn ends_with(This(this): This<Arc<String>>, suffix: Arc<String>) -> bool {
243    this.ends_with(suffix.as_str())
244}
245
246/// Returns true if a string matches the regular expression.
247///
248/// # Example
249/// ```cel
250/// "abc".matches("^[a-z]*$") == true
251/// ```
252#[cfg(feature = "regex")]
253pub fn matches(
254    ftx: &FunctionContext,
255    This(this): This<Arc<String>>,
256    regex: Arc<String>,
257) -> Result<bool> {
258    match regex::Regex::new(&regex) {
259        Ok(re) => Ok(re.is_match(&this)),
260        Err(err) => Err(ftx.error(format!("'{regex}' not a valid regex:\n{err}"))),
261    }
262}
263
264/// Returns true if the provided argument can be resolved.
265///
266/// This function is useful for checking if a property exists on a type before
267/// attempting to resolve it. Resolving a property that does not exist will
268/// result in a [`ExecutionError::NoSuchKey`] error.
269///
270/// Operates similar to the `has` macro describe in the Go CEL implementation
271/// spec: <https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros>.
272///
273/// # Examples
274/// ```cel
275/// has(foo.bar.baz)
276/// ```
277pub fn has(ftx: &FunctionContext) -> Result<Value> {
278    // We determine if a type has a property by attempting to resolve it.
279    // If we get a NoSuchKey error, then we know the property does not exist
280    match ftx.resolve(Argument(0)) {
281        Ok(_) => Value::Bool(true),
282        Err(err) => match err {
283            ExecutionError::NoSuchKey(_) => Value::Bool(false),
284            _ => return Err(err),
285        },
286    }
287    .into()
288}
289
290/// Maps the provided list to a new list by applying an expression to each
291/// input item.
292///
293/// This function is intended to be used like the CEL-go `map` macro:
294/// <https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros>
295///
296/// # Examples
297/// ```cel
298/// [1, 2, 3].map(x, x * 2) == [2, 4, 6]
299/// ```
300pub fn map(
301    ftx: &FunctionContext,
302    This(this): This<Value>,
303    ident: Identifier,
304    expr: Expression,
305) -> Result<Value> {
306    match this {
307        Value::List(items) => {
308            let mut values = Vec::with_capacity(items.len());
309            let mut ptx = ftx.ptx.new_inner_scope();
310            for item in items.iter() {
311                ptx.add_variable_from_value(ident.clone(), item.clone());
312                let value = ptx.resolve(&expr)?;
313                values.push(value);
314            }
315            Value::List(Arc::new(values))
316        }
317        Value::Map(map) => {
318            let mut values = Vec::with_capacity(map.map.len());
319            let mut ptx = ftx.ptx.new_inner_scope();
320            for (key, _) in map.map.iter() {
321                ptx.add_variable_from_value(ident.clone(), key.clone());
322                let value = ptx.resolve(&expr)?;
323                values.push(value);
324            }
325            Value::List(Arc::new(values))
326        }
327        _ => return Err(this.error_expected_type(ValueType::List)),
328    }
329    .into()
330}
331
332/// Filters the provided list by applying an expression to each input item
333/// and including the input item in the resulting list, only if the expression
334/// returned true.
335///
336/// This function is intended to be used like the CEL-go `filter` macro:
337/// <https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros>
338///
339/// # Example
340/// ```cel
341/// [1, 2, 3].filter(x, x > 1) == [2, 3]
342/// ```
343pub fn filter(
344    ftx: &FunctionContext,
345    This(this): This<Value>,
346    ident: Identifier,
347    expr: Expression,
348) -> Result<Value> {
349    match this {
350        Value::List(items) => {
351            let mut values = Vec::with_capacity(items.len());
352            let mut ptx = ftx.ptx.new_inner_scope();
353            for item in items.iter() {
354                ptx.add_variable_from_value(ident.clone(), item.clone());
355                if let Value::Bool(true) = ptx.resolve(&expr)? {
356                    values.push(item.clone());
357                }
358            }
359            Value::List(Arc::new(values))
360        }
361        _ => return Err(this.error_expected_type(ValueType::List)),
362    }
363    .into()
364}
365
366/// Returns a boolean value indicating whether every value in the provided
367/// list or map met the predicate defined by the provided expression. If
368/// called on a map, the predicate is applied to the map keys.
369///
370/// This function is intended to be used like the CEL-go `all` macro:
371/// <https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros>
372///
373/// # Example
374/// ```cel
375/// [1, 2, 3].all(x, x > 0) == true
376/// [{1:true, 2:true, 3:false}].all(x, x > 0) == true
377/// ```
378pub fn all(
379    ftx: &FunctionContext,
380    This(this): This<Value>,
381    ident: Identifier,
382    expr: Expression,
383) -> Result<bool> {
384    match this {
385        Value::List(items) => {
386            let mut ptx = ftx.ptx.new_inner_scope();
387            for item in items.iter() {
388                ptx.add_variable_from_value(&ident, item);
389                if let Value::Bool(false) = ptx.resolve(&expr)? {
390                    return Ok(false);
391                }
392            }
393            Ok(true)
394        }
395        Value::Map(value) => {
396            let mut ptx = ftx.ptx.new_inner_scope();
397            for key in value.map.keys() {
398                ptx.add_variable_from_value(&ident, key);
399                if let Value::Bool(false) = ptx.resolve(&expr)? {
400                    return Ok(false);
401                }
402            }
403            Ok(true)
404        }
405        _ => Err(this.error_expected_type(ValueType::List)),
406    }
407}
408
409/// Returns a boolean value indicating whether a or more values in the provided
410/// list or map meet the predicate defined by the provided expression.
411///
412/// If called on a map, the predicate is applied to the map keys.
413///
414/// This function is intended to be used like the CEL-go `exists` macro:
415/// <https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros>
416///
417/// # Example
418/// ```cel
419/// [1, 2, 3].exists(x, x > 0) == true
420/// [{1:true, 2:true, 3:false}].exists(x, x > 0) == true
421/// ```
422pub fn exists(
423    ftx: &FunctionContext,
424    This(this): This<Value>,
425    ident: Identifier,
426    expr: Expression,
427) -> Result<bool> {
428    match this {
429        Value::List(items) => {
430            let mut ptx = ftx.ptx.new_inner_scope();
431            for item in items.iter() {
432                ptx.add_variable_from_value(&ident, item);
433                if let Value::Bool(true) = ptx.resolve(&expr)? {
434                    return Ok(true);
435                }
436            }
437            Ok(false)
438        }
439        Value::Map(value) => {
440            let mut ptx = ftx.ptx.new_inner_scope();
441            for key in value.map.keys() {
442                ptx.add_variable_from_value(&ident, key);
443                if let Value::Bool(true) = ptx.resolve(&expr)? {
444                    return Ok(true);
445                }
446            }
447            Ok(false)
448        }
449        _ => Err(this.error_expected_type(ValueType::List)),
450    }
451}
452
453/// Returns a boolean value indicating whether only one value in the provided
454/// list or map meets the predicate defined by the provided expression.
455///
456/// If called on a map, the predicate is applied to the map keys.
457///
458/// This function is intended to be used like the CEL-go `exists` macro:
459/// <https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros>
460///
461/// # Example
462/// ```cel
463/// [1, 2, 3].exists_one(x, x > 0) == false
464/// [1, 2, 3].exists_one(x, x == 1) == true
465/// [{1:true, 2:true, 3:false}].exists_one(x, x > 0) == false
466/// ```
467pub fn exists_one(
468    ftx: &FunctionContext,
469    This(this): This<Value>,
470    ident: Identifier,
471    expr: Expression,
472) -> Result<bool> {
473    match this {
474        Value::List(items) => {
475            let mut ptx = ftx.ptx.new_inner_scope();
476            let mut exists = false;
477            for item in items.iter() {
478                ptx.add_variable_from_value(&ident, item);
479                if let Value::Bool(true) = ptx.resolve(&expr)? {
480                    if exists {
481                        return Ok(false);
482                    }
483                    exists = true;
484                }
485            }
486            Ok(exists)
487        }
488        Value::Map(value) => {
489            let mut ptx = ftx.ptx.new_inner_scope();
490            let mut exists = false;
491            for key in value.map.keys() {
492                ptx.add_variable_from_value(&ident, key);
493                if let Value::Bool(true) = ptx.resolve(&expr)? {
494                    if exists {
495                        return Ok(false);
496                    }
497                    exists = true;
498                }
499            }
500            Ok(exists)
501        }
502        _ => Err(this.error_expected_type(ValueType::List)),
503    }
504}
505
506#[cfg(feature = "chrono")]
507pub use time::duration;
508#[cfg(feature = "chrono")]
509pub use time::timestamp;
510
511#[cfg(feature = "chrono")]
512pub mod time {
513    use super::Result;
514    use crate::magic::This;
515    use crate::{ExecutionError, Value};
516    use chrono::{Datelike, Days, Months, Timelike};
517    use std::sync::Arc;
518
519    /// Duration parses the provided argument into a [`Value::Duration`] value.
520    ///
521    /// The argument must be string, and must be in the format of a duration. See
522    /// the [`parse_duration`] documentation for more information on the supported
523    /// formats.
524    ///
525    /// # Examples
526    /// - `1h` parses as 1 hour
527    /// - `1.5h` parses as 1 hour and 30 minutes
528    /// - `1h30m` parses as 1 hour and 30 minutes
529    /// - `1h30m1s` parses as 1 hour, 30 minutes, and 1 second
530    /// - `1ms` parses as 1 millisecond
531    /// - `1.5ms` parses as 1 millisecond and 500 microseconds
532    /// - `1ns` parses as 1 nanosecond
533    /// - `1.5ns` parses as 1 nanosecond (sub-nanosecond durations not supported)
534    pub fn duration(value: Arc<String>) -> crate::functions::Result<Value> {
535        Ok(Value::Duration(_duration(value.as_str())?))
536    }
537
538    /// Timestamp parses the provided argument into a [`Value::Timestamp`] value.
539    /// The
540    pub fn timestamp(value: Arc<String>) -> Result<Value> {
541        Ok(Value::Timestamp(
542            chrono::DateTime::parse_from_rfc3339(value.as_str())
543                .map_err(|e| ExecutionError::function_error("timestamp", e.to_string().as_str()))?,
544        ))
545    }
546
547    /// A wrapper around [`parse_duration`] that converts errors into [`ExecutionError`].
548    /// and only returns the duration, rather than returning the remaining input.
549    fn _duration(i: &str) -> Result<chrono::Duration> {
550        let (_, duration) = crate::duration::parse_duration(i)
551            .map_err(|e| ExecutionError::function_error("duration", e.to_string()))?;
552        Ok(duration)
553    }
554
555    fn _timestamp(i: &str) -> Result<chrono::DateTime<chrono::FixedOffset>> {
556        chrono::DateTime::parse_from_rfc3339(i)
557            .map_err(|e| ExecutionError::function_error("timestamp", e.to_string()))
558    }
559
560    pub fn timestamp_year(
561        This(this): This<chrono::DateTime<chrono::FixedOffset>>,
562    ) -> Result<Value> {
563        Ok(this.year().into())
564    }
565
566    pub fn timestamp_month(
567        This(this): This<chrono::DateTime<chrono::FixedOffset>>,
568    ) -> Result<Value> {
569        Ok((this.month0() as i32).into())
570    }
571
572    pub fn timestamp_year_day(
573        This(this): This<chrono::DateTime<chrono::FixedOffset>>,
574    ) -> Result<Value> {
575        let year = this
576            .checked_sub_days(Days::new(this.day0() as u64))
577            .unwrap()
578            .checked_sub_months(Months::new(this.month0()))
579            .unwrap();
580        Ok(this.signed_duration_since(year).num_days().into())
581    }
582
583    pub fn timestamp_month_day(
584        This(this): This<chrono::DateTime<chrono::FixedOffset>>,
585    ) -> Result<Value> {
586        Ok((this.day0() as i32).into())
587    }
588
589    pub fn timestamp_date(
590        This(this): This<chrono::DateTime<chrono::FixedOffset>>,
591    ) -> Result<Value> {
592        Ok((this.day() as i32).into())
593    }
594
595    pub fn timestamp_weekday(
596        This(this): This<chrono::DateTime<chrono::FixedOffset>>,
597    ) -> Result<Value> {
598        Ok((this.weekday().num_days_from_sunday() as i32).into())
599    }
600
601    pub fn timestamp_hours(
602        This(this): This<chrono::DateTime<chrono::FixedOffset>>,
603    ) -> Result<Value> {
604        Ok((this.hour() as i32).into())
605    }
606
607    pub fn timestamp_minutes(
608        This(this): This<chrono::DateTime<chrono::FixedOffset>>,
609    ) -> Result<Value> {
610        Ok((this.minute() as i32).into())
611    }
612
613    pub fn timestamp_seconds(
614        This(this): This<chrono::DateTime<chrono::FixedOffset>>,
615    ) -> Result<Value> {
616        Ok((this.second() as i32).into())
617    }
618
619    pub fn timestamp_millis(
620        This(this): This<chrono::DateTime<chrono::FixedOffset>>,
621    ) -> Result<Value> {
622        Ok((this.timestamp_subsec_millis() as i32).into())
623    }
624}
625
626pub fn max(Arguments(args): Arguments) -> Result<Value> {
627    // If items is a list of values, then operate on the list
628    let items = if args.len() == 1 {
629        match &args[0] {
630            Value::List(values) => values,
631            _ => return Ok(args[0].clone()),
632        }
633    } else {
634        &args
635    };
636
637    items
638        .iter()
639        .skip(1)
640        .try_fold(items.first().unwrap_or(&Value::Null), |acc, x| {
641            match acc.partial_cmp(x) {
642                Some(Ordering::Greater) => Ok(acc),
643                Some(_) => Ok(x),
644                None => Err(ExecutionError::ValuesNotComparable(acc.clone(), x.clone())),
645            }
646        })
647        .cloned()
648}
649
650pub fn min(Arguments(args): Arguments) -> Result<Value> {
651    // If items is a list of values, then operate on the list
652    let items = if args.len() == 1 {
653        match &args[0] {
654            Value::List(values) => values,
655            _ => return Ok(args[0].clone()),
656        }
657    } else {
658        &args
659    };
660
661    items
662        .iter()
663        .skip(1)
664        .try_fold(items.first().unwrap_or(&Value::Null), |acc, x| {
665            match acc.partial_cmp(x) {
666                Some(Ordering::Less) => Ok(acc),
667                Some(_) => Ok(x),
668                None => Err(ExecutionError::ValuesNotComparable(acc.clone(), x.clone())),
669            }
670        })
671        .cloned()
672}
673
674#[cfg(test)]
675mod tests {
676    use crate::context::Context;
677    use crate::tests::test_script;
678
679    fn assert_script(input: &(&str, &str)) {
680        assert_eq!(test_script(input.1, None), Ok(true.into()), "{}", input.0);
681    }
682
683    #[test]
684    fn test_size() {
685        [
686            ("size of list", "size([1, 2, 3]) == 3"),
687            ("size of map", "size({'a': 1, 'b': 2, 'c': 3}) == 3"),
688            ("size of string", "size('foo') == 3"),
689            ("size of bytes", "size(b'foo') == 3"),
690            ("size as a list method", "[1, 2, 3].size() == 3"),
691            ("size as a string method", "'foobar'.size() == 6"),
692        ]
693        .iter()
694        .for_each(assert_script);
695    }
696
697    #[test]
698    fn test_has() {
699        let tests = vec![
700            ("map has", "has(foo.bar) == true"),
701            ("map has", "has(foo.bar) == true"),
702            ("map not has", "has(foo.baz) == false"),
703            ("map deep not has", "has(foo.baz.bar) == false"),
704        ];
705
706        for (name, script) in tests {
707            let mut ctx = Context::default();
708            ctx.add_variable_from_value("foo", std::collections::HashMap::from([("bar", 1)]));
709            assert_eq!(test_script(script, Some(ctx)), Ok(true.into()), "{}", name);
710        }
711    }
712
713    #[test]
714    fn test_map() {
715        [
716            ("map list", "[1, 2, 3].map(x, x * 2) == [2, 4, 6]"),
717            ("map list 2", "[1, 2, 3].map(y, y + 1) == [2, 3, 4]"),
718            (
719                "nested map",
720                "[[1, 2], [2, 3]].map(x, x.map(x, x * 2)) == [[2, 4], [4, 6]]",
721            ),
722            (
723                "map to list",
724                r#"{'John': 'smart'}.map(key, key) == ['John']"#,
725            ),
726        ]
727        .iter()
728        .for_each(assert_script);
729    }
730
731    #[test]
732    fn test_filter() {
733        [("filter list", "[1, 2, 3].filter(x, x > 2) == [3]")]
734            .iter()
735            .for_each(assert_script);
736    }
737
738    #[test]
739    fn test_all() {
740        [
741            ("all list #1", "[0, 1, 2].all(x, x >= 0)"),
742            ("all list #2", "[0, 1, 2].all(x, x > 0) == false"),
743            ("all map", "{0: 0, 1:1, 2:2}.all(x, x >= 0) == true"),
744        ]
745        .iter()
746        .for_each(assert_script);
747    }
748
749    #[test]
750    fn test_exists() {
751        [
752            ("exist list #1", "[0, 1, 2].exists(x, x > 0)"),
753            ("exist list #2", "[0, 1, 2].exists(x, x == 3) == false"),
754            ("exist list #3", "[0, 1, 2, 2].exists(x, x == 2)"),
755            ("exist map", "{0: 0, 1:1, 2:2}.exists(x, x > 0)"),
756        ]
757        .iter()
758        .for_each(assert_script);
759    }
760
761    #[test]
762    fn test_exists_one() {
763        [
764            ("exist list #1", "[0, 1, 2].exists_one(x, x > 0) == false"),
765            ("exist list #2", "[0, 1, 2].exists_one(x, x == 0)"),
766            ("exist map", "{0: 0, 1:1, 2:2}.exists_one(x, x == 2)"),
767        ]
768        .iter()
769        .for_each(assert_script);
770    }
771
772    #[test]
773    fn test_max() {
774        [
775            ("max single", "max(1) == 1"),
776            ("max multiple", "max(1, 2, 3) == 3"),
777            ("max negative", "max(-1, 0) == 0"),
778            ("max float", "max(-1.0, 0.0) == 0.0"),
779            ("max list", "max([1, 2, 3]) == 3"),
780            ("max empty list", "max([]) == null"),
781            ("max no args", "max() == null"),
782        ]
783        .iter()
784        .for_each(assert_script);
785    }
786
787    #[test]
788    fn test_min() {
789        [
790            ("min single", "min(1) == 1"),
791            ("min multiple", "min(1, 2, 3) == 1"),
792            ("min negative", "min(-1, 0) == -1"),
793            ("min float", "min(-1.0, 0.0) == -1.0"),
794            (
795                "min float multiple",
796                "min(1.61803, 3.1415, 2.71828, 1.41421) == 1.41421",
797            ),
798            ("min list", "min([1, 2, 3]) == 1"),
799            ("min empty list", "min([]) == null"),
800            ("min no args", "min() == null"),
801        ]
802        .iter()
803        .for_each(assert_script);
804    }
805
806    #[test]
807    fn test_starts_with() {
808        [
809            ("starts with true", "'foobar'.startsWith('foo') == true"),
810            ("starts with false", "'foobar'.startsWith('bar') == false"),
811        ]
812        .iter()
813        .for_each(assert_script);
814    }
815
816    #[test]
817    fn test_ends_with() {
818        [
819            ("ends with true", "'foobar'.endsWith('bar') == true"),
820            ("ends with false", "'foobar'.endsWith('foo') == false"),
821        ]
822        .iter()
823        .for_each(assert_script);
824    }
825
826    #[cfg(feature = "chrono")]
827    #[test]
828    fn test_timestamp() {
829        [(
830                "comparison",
831                "timestamp('2023-05-29T00:00:00Z') > timestamp('2023-05-28T00:00:00Z')",
832            ),
833            (
834                "comparison",
835                "timestamp('2023-05-29T00:00:00Z') < timestamp('2023-05-30T00:00:00Z')",
836            ),
837            (
838                "subtracting duration",
839                "timestamp('2023-05-29T00:00:00Z') - duration('24h') == timestamp('2023-05-28T00:00:00Z')",
840            ),
841            (
842                "subtracting date",
843                "timestamp('2023-05-29T00:00:00Z') - timestamp('2023-05-28T00:00:00Z') == duration('24h')",
844            ),
845            (
846                "adding duration",
847                "timestamp('2023-05-28T00:00:00Z') + duration('24h') == timestamp('2023-05-29T00:00:00Z')",
848            ),
849            (
850                "timestamp string",
851                "timestamp('2023-05-28T00:00:00Z').string() == '2023-05-28T00:00:00+00:00'",
852            ),
853            (
854                "timestamp getFullYear",
855                "timestamp('2023-05-28T00:00:00Z').getFullYear() == 2023",
856            ),
857            (
858                "timestamp getMonth",
859                "timestamp('2023-05-28T00:00:00Z').getMonth() == 4",
860            ),
861            (
862                "timestamp getDayOfMonth",
863                "timestamp('2023-05-28T00:00:00Z').getDayOfMonth() == 27",
864            ),
865            (
866                "timestamp getDayOfYear",
867                "timestamp('2023-05-28T00:00:00Z').getDayOfYear() == 147",
868            ),
869            (
870                "timestamp getDate",
871                "timestamp('2023-05-28T00:00:00Z').getDate() == 28",
872            ),
873            (
874                "timestamp getDayOfWeek",
875                "timestamp('2023-05-28T00:00:00Z').getDayOfWeek() == 0",
876            ),
877            (
878                "timestamp getHours",
879                "timestamp('2023-05-28T02:00:00Z').getHours() == 2",
880            ),
881            (
882                "timestamp getMinutes",
883                " timestamp('2023-05-28T00:05:00Z').getMinutes() == 5",
884            ),
885            (
886                "timestamp getSeconds",
887                "timestamp('2023-05-28T00:00:06Z').getSeconds() == 6",
888            ),
889            (
890                "timestamp getMilliseconds",
891                "timestamp('2023-05-28T00:00:42.123Z').getMilliseconds() == 123",
892            ),
893
894        ]
895        .iter()
896        .for_each(assert_script);
897    }
898
899    #[cfg(feature = "chrono")]
900    #[test]
901    fn test_duration() {
902        [
903            ("duration equal 1", "duration('1s') == duration('1000ms')"),
904            ("duration equal 2", "duration('1m') == duration('60s')"),
905            ("duration equal 3", "duration('1h') == duration('60m')"),
906            ("duration comparison 1", "duration('1m') > duration('1s')"),
907            ("duration comparison 2", "duration('1m') < duration('1h')"),
908            (
909                "duration subtraction",
910                "duration('1h') - duration('1m') == duration('59m')",
911            ),
912            (
913                "duration addition",
914                "duration('1h') + duration('1m') == duration('1h1m')",
915            ),
916        ]
917        .iter()
918        .for_each(assert_script);
919    }
920
921    #[cfg(feature = "chrono")]
922    #[test]
923    fn test_timestamp_variable() {
924        let mut context = Context::default();
925        let ts: chrono::DateTime<chrono::FixedOffset> =
926            chrono::DateTime::parse_from_rfc3339("2023-05-29T00:00:00Z").unwrap();
927        context
928            .add_variable("ts", crate::Value::Timestamp(ts))
929            .unwrap();
930
931        let program = crate::Program::compile("ts == timestamp('2023-05-29T00:00:00Z')").unwrap();
932        let result = program.execute(&context).unwrap();
933        assert_eq!(result, true.into());
934    }
935
936    #[cfg(feature = "chrono")]
937    #[test]
938    fn test_chrono_string() {
939        [
940            ("duration", "duration('1h30m').string() == '1h30m0s'"),
941            (
942                "timestamp",
943                "timestamp('2023-05-29T00:00:00Z').string() == '2023-05-29T00:00:00+00:00'",
944            ),
945        ]
946        .iter()
947        .for_each(assert_script);
948    }
949
950    #[test]
951    fn test_contains() {
952        let tests = vec![
953            ("list", "[1, 2, 3].contains(3) == true"),
954            ("map", "{1: true, 2: true, 3: true}.contains(3) == true"),
955            ("string", "'foobar'.contains('bar') == true"),
956            ("bytes", "b'foobar'.contains(b'o') == true"),
957        ];
958
959        for (name, script) in tests {
960            assert_eq!(test_script(script, None), Ok(true.into()), "{}", name);
961        }
962    }
963
964    #[cfg(feature = "regex")]
965    #[test]
966    fn test_matches() {
967        let tests = vec![
968            ("string", "'foobar'.matches('^[a-zA-Z]*$') == true"),
969            (
970                "map",
971                "{'1': 'abc', '2': 'def', '3': 'ghi'}.all(key, key.matches('^[a-zA-Z]*$')) == false",
972            ),
973        ];
974
975        for (name, script) in tests {
976            assert_eq!(
977                test_script(script, None),
978                Ok(true.into()),
979                ".matches failed for '{name}'"
980            );
981        }
982    }
983
984    #[cfg(feature = "regex")]
985    #[test]
986    fn test_matches_err() {
987        assert_eq!(
988            test_script(
989                "'foobar'.matches('(foo') == true", None),
990            Err(
991                crate::ExecutionError::FunctionError {
992                    function: "matches".to_string(),
993                    message: "'(foo' not a valid regex:\nregex parse error:\n    (foo\n    ^\nerror: unclosed group".to_string()
994                }
995            )
996        );
997    }
998
999    #[test]
1000    fn test_string() {
1001        [
1002            ("string", "'foo'.string() == 'foo'"),
1003            ("int", "10.string() == '10'"),
1004            ("float", "10.5.string() == '10.5'"),
1005            ("bytes", "b'foo'.string() == 'foo'"),
1006        ]
1007        .iter()
1008        .for_each(assert_script);
1009    }
1010
1011    #[test]
1012    fn test_bytes() {
1013        [
1014            ("string", "bytes('abc') == b'abc'"),
1015            ("bytes", "bytes('abc') == b'\\x61b\\x63'"),
1016        ]
1017        .iter()
1018        .for_each(assert_script);
1019    }
1020
1021    #[test]
1022    fn test_double() {
1023        [
1024            ("string", "'10'.double() == 10.0"),
1025            ("int", "10.double() == 10.0"),
1026            ("double", "10.0.double() == 10.0"),
1027        ]
1028        .iter()
1029        .for_each(assert_script);
1030    }
1031
1032    #[test]
1033    fn test_uint() {
1034        [
1035            ("string", "'10'.uint() == 10.uint()"),
1036            ("double", "10.5.uint() == 10.uint()"),
1037        ]
1038        .iter()
1039        .for_each(assert_script);
1040    }
1041
1042    #[test]
1043    fn test_int() {
1044        [
1045            ("string", "'10'.int() == 10"),
1046            ("int", "10.int() == 10"),
1047            ("uint", "10.uint().int() == 10"),
1048            ("double", "10.5.int() == 10"),
1049        ]
1050        .iter()
1051        .for_each(assert_script);
1052    }
1053}