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#[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 pub fn resolve<R>(&self, resolver: R) -> Result<Value>
46 where
47 R: Resolver,
48 {
49 resolver.resolve(self)
50 }
51
52 pub fn error<M: ToString>(&self, message: M) -> ExecutionError {
54 ExecutionError::function_error(self.name.as_str(), message)
55 }
56}
57
58pub 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
88pub 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
156pub 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::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
188pub 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 v => return Err(ftx.error(format!("cannot convert {v:?} to Float"))),
199 })
200}
201
202pub 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 v => return Err(ftx.error(format!("cannot convert {v:?} to int"))),
240 })
241}
242
243pub fn starts_with(This(this): This<Arc<String>>, prefix: Arc<String>) -> bool {
250 this.starts_with(prefix.as_str())
251}
252
253pub fn ends_with(This(this): This<Arc<String>>, suffix: Arc<String>) -> bool {
260 this.ends_with(suffix.as_str())
261}
262
263#[cfg(feature = "regex")]
270pub fn matches(
271 This(this): This<Arc<String>>,
273 regex: regex::Regex,
274) -> bool {
275 regex.is_match(&this)
276 }
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#[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 pub fn duration(value: Arc<String>) -> crate::functions::Result<Value> {
339 Ok(Value::Duration(_duration(value.as_str())?))
340 }
341
342 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 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 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 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]
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: "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]
832 fn test_int() {
833 [
834 ("String", r#"Int("10") == 10"#),
835 ("Int", "Int(10) == 10"),
836 ("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 ("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}