use crate::context::Context;
use crate::magic::{Arguments, This};
use crate::objects::{KeyRef, OptionalValue, Value};
use crate::parser::Expression;
use crate::resolvers::Resolver;
use crate::ExecutionError;
use std::cmp::Ordering;
use std::convert::TryInto;
use std::sync::Arc;
type Result<T> = std::result::Result<T, ExecutionError>;
#[derive(Clone)]
pub struct FunctionContext<'context, 'call: 'context> {
pub name: &'call str,
pub this: Option<Value>,
pub ptx: &'context Context<'context>,
pub args: &'call [Expression],
pub arg_idx: usize,
}
impl<'context, 'call: 'context> FunctionContext<'context, 'call> {
pub fn new(
name: &'call str,
this: Option<Value>,
ptx: &'context Context<'context>,
args: &'call [Expression],
) -> Self {
Self {
name,
this,
ptx,
args,
arg_idx: 0,
}
}
pub fn resolve<R>(&self, resolver: R) -> Result<Value>
where
R: Resolver,
{
resolver.resolve(self)
}
pub fn error<M: ToString>(&self, message: M) -> ExecutionError {
ExecutionError::function_error(self.name, message)
}
}
pub fn size(ftx: &FunctionContext, This(this): This<Value>) -> Result<i64> {
let size = match this {
Value::List(l) => l.len(),
Value::Map(m) => m.map.len(),
Value::String(s) => s.len(),
Value::Bytes(b) => b.len(),
value => return Err(ftx.error(format!("cannot determine the size of {value:?}"))),
};
Ok(size as i64)
}
pub fn contains(This(this): This<Value>, arg: Value) -> Result<Value> {
Ok(match this {
Value::List(v) => v.contains(&arg),
Value::Map(v) => {
v.contains_key(&KeyRef::try_from(&arg).map_err(ExecutionError::UnsupportedKeyType)?)
}
Value::String(s) => {
if let Value::String(arg) = arg {
s.contains(arg.as_str())
} else {
false
}
}
Value::Bytes(b) => {
if let Value::Bytes(arg) = arg {
let s = arg.as_slice();
b.windows(arg.len()).any(|w| w == s)
} else {
false
}
}
_ => false,
}
.into())
}
pub fn string(ftx: &FunctionContext, This(this): This<Value>) -> Result<Value> {
Ok(match this {
Value::String(v) => Value::String(v.clone()),
#[cfg(feature = "chrono")]
Value::Timestamp(t) => Value::String(t.to_rfc3339().into()),
#[cfg(feature = "chrono")]
Value::Duration(v) => Value::String(crate::duration::format_duration(&v).into()),
Value::Int(v) => Value::String(v.to_string().into()),
Value::UInt(v) => Value::String(v.to_string().into()),
Value::Float(v) => Value::String(v.to_string().into()),
Value::Bytes(v) => Value::String(Arc::new(String::from_utf8_lossy(v.as_slice()).into())),
v => return Err(ftx.error(format!("cannot convert {v:?} to string"))),
})
}
pub fn bytes(value: Arc<String>) -> Result<Value> {
Ok(Value::Bytes(value.as_bytes().to_vec().into()))
}
pub fn double(ftx: &FunctionContext, This(this): This<Value>) -> Result<Value> {
Ok(match this {
Value::String(v) => v
.parse::<f64>()
.map(Value::Float)
.map_err(|e| ftx.error(format!("string parse error: {e}")))?,
Value::Float(v) => Value::Float(v),
Value::Int(v) => Value::Float(v as f64),
Value::UInt(v) => Value::Float(v as f64),
v => return Err(ftx.error(format!("cannot convert {v:?} to double"))),
})
}
pub fn uint(ftx: &FunctionContext, This(this): This<Value>) -> Result<Value> {
Ok(match this {
Value::String(v) => v
.parse::<u64>()
.map(Value::UInt)
.map_err(|e| ftx.error(format!("string parse error: {e}")))?,
Value::Float(v) => {
if v > u64::MAX as f64 || v < u64::MIN as f64 {
return Err(ftx.error("unsigned integer overflow"));
}
Value::UInt(v as u64)
}
Value::Int(v) => Value::UInt(
v.try_into()
.map_err(|_| ftx.error("unsigned integer overflow"))?,
),
Value::UInt(v) => Value::UInt(v),
v => return Err(ftx.error(format!("cannot convert {v:?} to uint"))),
})
}
pub fn int(ftx: &FunctionContext, This(this): This<Value>) -> Result<Value> {
Ok(match this {
Value::String(v) => v
.parse::<i64>()
.map(Value::Int)
.map_err(|e| ftx.error(format!("string parse error: {e}")))?,
Value::Float(v) => {
if v > i64::MAX as f64 || v < i64::MIN as f64 {
return Err(ftx.error("integer overflow"));
}
Value::Int(v as i64)
}
Value::Int(v) => Value::Int(v),
Value::UInt(v) => Value::Int(v.try_into().map_err(|_| ftx.error("integer overflow"))?),
v => return Err(ftx.error(format!("cannot convert {v:?} to int"))),
})
}
pub fn optional_none(ftx: &FunctionContext) -> Result<Value> {
if ftx.this.is_some() || !ftx.args.is_empty() {
return Err(ftx.error("unsupported function"));
}
Ok(Value::Opaque(Arc::new(OptionalValue::none())))
}
pub fn optional_of(ftx: &FunctionContext, value: Value) -> Result<Value> {
if ftx.this.is_some() {
return Err(ftx.error("unsupported function"));
}
Ok(Value::Opaque(Arc::new(OptionalValue::of(value))))
}
pub fn optional_of_non_zero_value(ftx: &FunctionContext, value: Value) -> Result<Value> {
if ftx.this.is_some() {
return Err(ftx.error("unsupported function"));
}
if value.is_zero() {
Ok(Value::Opaque(Arc::new(OptionalValue::none())))
} else {
Ok(Value::Opaque(Arc::new(OptionalValue::of(value))))
}
}
pub fn optional_value(This(this): This<Value>) -> Result<Value> {
<&OptionalValue>::try_from(&this)?
.value()
.cloned()
.ok_or_else(|| ExecutionError::function_error("value", "optional.none() dereference"))
}
pub fn optional_has_value(This(this): This<Value>) -> Result<bool> {
Ok(<&OptionalValue>::try_from(&this)?.value().is_some())
}
pub fn optional_or_optional(This(this): This<Value>, other: Value) -> Result<Value> {
let this_opt: &OptionalValue = (&this).try_into()?;
match this_opt.value() {
Some(_) => Ok(this),
None => {
let _: &OptionalValue = (&other).try_into()?;
Ok(other)
}
}
}
pub fn optional_or_value(This(this): This<Value>, other: Value) -> Result<Value> {
let this_opt: &OptionalValue = (&this).try_into()?;
match this_opt.value() {
Some(v) => Ok(v.clone()),
None => Ok(other),
}
}
pub fn starts_with(This(this): This<Arc<String>>, prefix: Arc<String>) -> bool {
this.starts_with(prefix.as_str())
}
pub fn ends_with(This(this): This<Arc<String>>, suffix: Arc<String>) -> bool {
this.ends_with(suffix.as_str())
}
#[cfg(feature = "regex")]
pub fn matches(
ftx: &FunctionContext,
This(this): This<Arc<String>>,
regex: Arc<String>,
) -> Result<bool> {
match regex::Regex::new(®ex) {
Ok(re) => Ok(re.is_match(&this)),
Err(err) => Err(ftx.error(format!("'{regex}' not a valid regex:\n{err}"))),
}
}
#[cfg(feature = "chrono")]
pub use time::duration;
#[cfg(feature = "chrono")]
pub use time::timestamp;
#[cfg(feature = "chrono")]
pub mod time {
use super::Result;
use crate::magic::This;
use crate::{ExecutionError, Value};
use chrono::{Datelike, Days, Months, Timelike};
use std::sync::Arc;
pub fn duration(value: Arc<String>) -> crate::functions::Result<Value> {
Ok(Value::Duration(_duration(value.as_str())?))
}
pub fn timestamp(value: Arc<String>) -> Result<Value> {
Ok(Value::Timestamp(
chrono::DateTime::parse_from_rfc3339(value.as_str())
.map_err(|e| ExecutionError::function_error("timestamp", e.to_string().as_str()))?,
))
}
fn _duration(i: &str) -> Result<chrono::Duration> {
let (_, duration) = crate::duration::parse_duration(i)
.map_err(|e| ExecutionError::function_error("duration", e.to_string()))?;
Ok(duration)
}
fn _timestamp(i: &str) -> Result<chrono::DateTime<chrono::FixedOffset>> {
chrono::DateTime::parse_from_rfc3339(i)
.map_err(|e| ExecutionError::function_error("timestamp", e.to_string()))
}
pub fn timestamp_year(
This(this): This<chrono::DateTime<chrono::FixedOffset>>,
) -> Result<Value> {
Ok(this.year().into())
}
pub fn timestamp_month(
This(this): This<chrono::DateTime<chrono::FixedOffset>>,
) -> Result<Value> {
Ok((this.month0() as i32).into())
}
pub fn timestamp_year_day(
This(this): This<chrono::DateTime<chrono::FixedOffset>>,
) -> Result<Value> {
let year = this
.checked_sub_days(Days::new(this.day0() as u64))
.unwrap()
.checked_sub_months(Months::new(this.month0()))
.unwrap();
Ok(this.signed_duration_since(year).num_days().into())
}
pub fn timestamp_month_day(
This(this): This<chrono::DateTime<chrono::FixedOffset>>,
) -> Result<Value> {
Ok((this.day0() as i32).into())
}
pub fn timestamp_date(
This(this): This<chrono::DateTime<chrono::FixedOffset>>,
) -> Result<Value> {
Ok((this.day() as i32).into())
}
pub fn timestamp_weekday(
This(this): This<chrono::DateTime<chrono::FixedOffset>>,
) -> Result<Value> {
Ok((this.weekday().num_days_from_sunday() as i32).into())
}
pub fn get_hours(This(this): This<Value>) -> Result<Value> {
Ok(match this {
Value::Timestamp(ts) => (ts.hour() as i32).into(),
Value::Duration(d) => (d.num_hours() as i32).into(),
_ => {
return Err(ExecutionError::function_error(
"getHours",
"expected timestamp or duration",
))
}
})
}
pub fn get_minutes(This(this): This<Value>) -> Result<Value> {
Ok(match this {
Value::Timestamp(ts) => (ts.minute() as i32).into(),
Value::Duration(d) => (d.num_minutes() as i32).into(),
_ => {
return Err(ExecutionError::function_error(
"getMinutes",
"expected timestamp or duration",
))
}
})
}
pub fn get_seconds(This(this): This<Value>) -> Result<Value> {
Ok(match this {
Value::Timestamp(ts) => (ts.second() as i32).into(),
Value::Duration(d) => (d.num_seconds() as i32).into(),
_ => {
return Err(ExecutionError::function_error(
"getSeconds",
"expected timestamp or duration",
))
}
})
}
pub fn get_milliseconds(This(this): This<Value>) -> Result<Value> {
Ok(match this {
Value::Timestamp(ts) => (ts.timestamp_subsec_millis() as i32).into(),
Value::Duration(d) => (d.num_milliseconds() as i32).into(),
_ => {
return Err(ExecutionError::function_error(
"getMilliseconds",
"expected timestamp or duration",
))
}
})
}
}
pub fn max(Arguments(args): Arguments) -> Result<Value> {
let items = if args.len() == 1 {
match &args[0] {
Value::List(values) => values,
_ => return Ok(args[0].clone()),
}
} else {
&args
};
items
.iter()
.skip(1)
.try_fold(items.first().unwrap_or(&Value::Null), |acc, x| {
match acc.partial_cmp(x) {
Some(Ordering::Greater) => Ok(acc),
Some(_) => Ok(x),
None => Err(ExecutionError::ValuesNotComparable(acc.clone(), x.clone())),
}
})
.cloned()
}
pub fn min(Arguments(args): Arguments) -> Result<Value> {
let items = if args.len() == 1 {
match &args[0] {
Value::List(values) => values,
_ => return Ok(args[0].clone()),
}
} else {
&args
};
items
.iter()
.skip(1)
.try_fold(items.first().unwrap_or(&Value::Null), |acc, x| {
match acc.partial_cmp(x) {
Some(Ordering::Less) => Ok(acc),
Some(_) => Ok(x),
None => Err(ExecutionError::ValuesNotComparable(acc.clone(), x.clone())),
}
})
.cloned()
}
#[cfg(test)]
mod tests {
use crate::context::Context;
use crate::tests::test_script;
fn assert_script(input: &(&str, &str)) {
assert_eq!(test_script(input.1, None), Ok(true.into()), "{}", input.0);
}
fn assert_error(input: &(&str, &str, &str)) {
assert_eq!(
test_script(input.1, None).map_err(|e| e.to_string()),
Err(input.2.to_string()),
"{}",
input.0
);
}
#[test]
fn test_size() {
[
("size of list", "size([1, 2, 3]) == 3"),
("size of map", "size({'a': 1, 'b': 2, 'c': 3}) == 3"),
("size of string", "size('foo') == 3"),
("size of bytes", "size(b'foo') == 3"),
("size as a list method", "[1, 2, 3].size() == 3"),
("size as a string method", "'foobar'.size() == 6"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_has() {
let tests = vec![
("map has", "has(foo.bar) == true"),
("map not has", "has(foo.baz) == false"),
];
for (name, script) in tests {
let mut ctx = Context::default();
ctx.add_variable_from_value("foo", std::collections::HashMap::from([("bar", 1)]));
assert_eq!(test_script(script, Some(ctx)), Ok(true.into()), "{name}");
}
}
#[test]
fn test_map() {
[
("map list", "[1, 2, 3].map(x, x * 2) == [2, 4, 6]"),
("map list 2", "[1, 2, 3].map(y, y + 1) == [2, 3, 4]"),
(
"map list filter",
"[1, 2, 3].map(y, y % 2 == 0, y + 1) == [3]",
),
(
"nested map",
"[[1, 2], [2, 3]].map(x, x.map(x, x * 2)) == [[2, 4], [4, 6]]",
),
(
"map to list",
r#"{'John': 'smart'}.map(key, key) == ['John']"#,
),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_filter() {
[("filter list", "[1, 2, 3].filter(x, x > 2) == [3]")]
.iter()
.for_each(assert_script);
}
#[test]
fn test_all() {
[
("all list #1", "[0, 1, 2].all(x, x >= 0)"),
("all list #2", "[0, 1, 2].all(x, x > 0) == false"),
("all map", "{0: 0, 1:1, 2:2}.all(x, x >= 0) == true"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_exists() {
[
("exist list #1", "[0, 1, 2].exists(x, x > 0)"),
("exist list #2", "[0, 1, 2].exists(x, x == 3) == false"),
("exist list #3", "[0, 1, 2, 2].exists(x, x == 2)"),
("exist map", "{0: 0, 1:1, 2:2}.exists(x, x > 0)"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_exists_one() {
[
("exist list #1", "[0, 1, 2].exists_one(x, x > 0) == false"),
("exist list #2", "[0, 1, 2].exists_one(x, x == 0)"),
("exist map", "{0: 0, 1:1, 2:2}.exists_one(x, x == 2)"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_max() {
[
("max single", "max(1) == 1"),
("max multiple", "max(1, 2, 3) == 3"),
("max negative", "max(-1, 0) == 0"),
("max float", "max(-1.0, 0.0) == 0.0"),
("max list", "max([1, 2, 3]) == 3"),
("max empty list", "max([]) == null"),
("max no args", "max() == null"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_min() {
[
("min single", "min(1) == 1"),
("min multiple", "min(1, 2, 3) == 1"),
("min negative", "min(-1, 0) == -1"),
("min float", "min(-1.0, 0.0) == -1.0"),
(
"min float multiple",
"min(1.61803, 3.1415, 2.71828, 1.41421) == 1.41421",
),
("min list", "min([1, 2, 3]) == 1"),
("min empty list", "min([]) == null"),
("min no args", "min() == null"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_starts_with() {
[
("starts with true", "'foobar'.startsWith('foo') == true"),
("starts with false", "'foobar'.startsWith('bar') == false"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_ends_with() {
[
("ends with true", "'foobar'.endsWith('bar') == true"),
("ends with false", "'foobar'.endsWith('foo') == false"),
]
.iter()
.for_each(assert_script);
}
#[cfg(feature = "chrono")]
#[test]
fn test_timestamp() {
[(
"comparison",
"timestamp('2023-05-29T00:00:00Z') > timestamp('2023-05-28T00:00:00Z')",
),
(
"comparison",
"timestamp('2023-05-29T00:00:00Z') < timestamp('2023-05-30T00:00:00Z')",
),
(
"subtracting duration",
"timestamp('2023-05-29T00:00:00Z') - duration('24h') == timestamp('2023-05-28T00:00:00Z')",
),
(
"subtracting date",
"timestamp('2023-05-29T00:00:00Z') - timestamp('2023-05-28T00:00:00Z') == duration('24h')",
),
(
"adding duration",
"timestamp('2023-05-28T00:00:00Z') + duration('24h') == timestamp('2023-05-29T00:00:00Z')",
),
(
"timestamp string",
"timestamp('2023-05-28T00:00:00Z').string() == '2023-05-28T00:00:00+00:00'",
),
(
"timestamp getFullYear",
"timestamp('2023-05-28T00:00:00Z').getFullYear() == 2023",
),
(
"timestamp getMonth",
"timestamp('2023-05-28T00:00:00Z').getMonth() == 4",
),
(
"timestamp getDayOfMonth",
"timestamp('2023-05-28T00:00:00Z').getDayOfMonth() == 27",
),
(
"timestamp getDayOfYear",
"timestamp('2023-05-28T00:00:00Z').getDayOfYear() == 147",
),
(
"timestamp getDate",
"timestamp('2023-05-28T00:00:00Z').getDate() == 28",
),
(
"timestamp getDayOfWeek",
"timestamp('2023-05-28T00:00:00Z').getDayOfWeek() == 0",
),
(
"timestamp getHours",
"timestamp('2023-05-28T02:00:00Z').getHours() == 2",
),
(
"timestamp getMinutes",
" timestamp('2023-05-28T00:05:00Z').getMinutes() == 5",
),
(
"timestamp getSeconds",
"timestamp('2023-05-28T00:00:06Z').getSeconds() == 6",
),
(
"timestamp getMilliseconds",
"timestamp('2023-05-28T00:00:42.123Z').getMilliseconds() == 123",
),
]
.iter()
.for_each(assert_script);
[
(
"timestamp out of range",
"timestamp('0000-01-00T00:00:00Z')",
"Error executing function 'timestamp': input is out of range",
),
(
"timestamp out of range",
"timestamp('9999-12-32T23:59:59.999999999Z')",
"Error executing function 'timestamp': input is out of range",
),
(
"timestamp overflow",
"timestamp('9999-12-31T23:59:59Z') + duration('1s')",
"Overflow from binary operator 'add': Timestamp(9999-12-31T23:59:59+00:00), Duration(TimeDelta { secs: 1, nanos: 0 })",
),
(
"timestamp underflow",
"timestamp('0001-01-01T00:00:00Z') - duration('1s')",
"Overflow from binary operator 'sub': Timestamp(0001-01-01T00:00:00+00:00), Duration(TimeDelta { secs: 1, nanos: 0 })",
),
(
"timestamp underflow",
"timestamp('0001-01-01T00:00:00Z') + duration('-1s')",
"Overflow from binary operator 'add': Timestamp(0001-01-01T00:00:00+00:00), Duration(TimeDelta { secs: -1, nanos: 0 })",
)
]
.iter()
.for_each(assert_error)
}
#[cfg(feature = "chrono")]
#[test]
fn test_duration() {
[
("duration equal 1", "duration('1s') == duration('1000ms')"),
("duration equal 2", "duration('1m') == duration('60s')"),
("duration equal 3", "duration('1h') == duration('60m')"),
("duration comparison 1", "duration('1m') > duration('1s')"),
("duration comparison 2", "duration('1m') < duration('1h')"),
(
"duration subtraction",
"duration('1h') - duration('1m') == duration('59m')",
),
(
"duration addition",
"duration('1h') + duration('1m') == duration('1h1m')",
),
("duration getHours", "duration('2h30m45s').getHours() == 2"),
(
"duration getMinutes",
"duration('2h30m45s').getMinutes() == 150",
),
(
"duration getSeconds",
"duration('2h30m45s').getSeconds() == 9045",
),
(
"duration getMilliseconds",
"duration('1s500ms').getMilliseconds() == 1500",
),
(
"duration getHours overflow",
"duration('25h').getHours() == 25",
),
(
"duration getMinutes overflow",
"duration('90m').getMinutes() == 90",
),
(
"duration getSeconds overflow",
"duration('90s').getSeconds() == 90",
),
]
.iter()
.for_each(assert_script);
}
#[cfg(feature = "chrono")]
#[test]
fn test_timestamp_variable() {
let mut context = Context::default();
let ts: chrono::DateTime<chrono::FixedOffset> =
chrono::DateTime::parse_from_rfc3339("2023-05-29T00:00:00Z").unwrap();
context
.add_variable("ts", crate::Value::Timestamp(ts))
.unwrap();
let program = crate::Program::compile("ts == timestamp('2023-05-29T00:00:00Z')").unwrap();
let result = program.execute(&context).unwrap();
assert_eq!(result, true.into());
}
#[cfg(feature = "chrono")]
#[test]
fn test_chrono_string() {
[
("duration", "duration('1h30m').string() == '1h30m0s'"),
(
"timestamp",
"timestamp('2023-05-29T00:00:00Z').string() == '2023-05-29T00:00:00+00:00'",
),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_contains() {
let tests = vec![
("list", "[1, 2, 3].contains(3) == true"),
("map", "{1: true, 2: true, 3: true}.contains(3) == true"),
("string", "'foobar'.contains('bar') == true"),
("bytes", "b'foobar'.contains(b'o') == true"),
];
for (name, script) in tests {
assert_eq!(test_script(script, None), Ok(true.into()), "{name}");
}
}
#[cfg(feature = "regex")]
#[test]
fn test_matches() {
let tests = vec![
("string", "'foobar'.matches('^[a-zA-Z]*$') == true"),
(
"map",
"{'1': 'abc', '2': 'def', '3': 'ghi'}.all(key, key.matches('^[a-zA-Z]*$')) == false",
),
];
for (name, script) in tests {
assert_eq!(
test_script(script, None),
Ok(true.into()),
".matches failed for '{name}'"
);
}
}
#[cfg(feature = "regex")]
#[test]
fn test_matches_err() {
assert_eq!(
test_script(
"'foobar'.matches('(foo') == true", None),
Err(
crate::ExecutionError::FunctionError {
function: "matches".to_string(),
message: "'(foo' not a valid regex:\nregex parse error:\n (foo\n ^\nerror: unclosed group".to_string()
}
)
);
}
#[test]
fn test_string() {
[
("string", "'foo'.string() == 'foo'"),
("int", "10.string() == '10'"),
("float", "10.5.string() == '10.5'"),
("bytes", "b'foo'.string() == 'foo'"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_bytes() {
[
("string", "bytes('abc') == b'abc'"),
("bytes", "bytes('abc') == b'\\x61b\\x63'"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_double() {
[
("string", "'10'.double() == 10.0"),
("int", "10.double() == 10.0"),
("double", "10.0.double() == 10.0"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_uint() {
[
("string", "'10'.uint() == 10.uint()"),
("double", "10.5.uint() == 10.uint()"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn test_int() {
[
("string", "'10'.int() == 10"),
("int", "10.int() == 10"),
("uint", "10.uint().int() == 10"),
("double", "10.5.int() == 10"),
]
.iter()
.for_each(assert_script);
}
#[test]
fn no_bool_coercion() {
[
("string || bool", "'' || false", "No such overload"),
("int || bool", "1 || false", "No such overload"),
("int || bool", "1u || false", "No such overload"),
("float || bool", "0.1|| false", "No such overload"),
("list || bool", "[] || false", "No such overload"),
("map || bool", "{} || false", "No such overload"),
("null || bool", "null || false", "No such overload"),
]
.iter()
.for_each(assert_error)
}
}