use crate::{Evaluation, EvaluationSema, SpannedExpr};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Unknown function: {0}")]
UnknownFunction(String),
#[error("Incorrect arguments for function {0:?}: expected {1}")]
Arity(Function, &'static str),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Function {
Contains,
StartsWith,
EndsWith,
Format,
Join,
ToJSON,
FromJSON,
HashFiles,
Case,
Success,
Always,
Cancelled,
Failure,
}
impl Function {
pub(crate) fn new(name: &str) -> Option<Self> {
match name.to_ascii_lowercase().as_str() {
"contains" => Some(Self::Contains),
"startswith" => Some(Self::StartsWith),
"endswith" => Some(Self::EndsWith),
"format" => Some(Self::Format),
"join" => Some(Self::Join),
"tojson" => Some(Self::ToJSON),
"fromjson" => Some(Self::FromJSON),
"hashfiles" => Some(Self::HashFiles),
"case" => Some(Self::Case),
"success" => Some(Self::Success),
"always" => Some(Self::Always),
"cancelled" => Some(Self::Cancelled),
"failure" => Some(Self::Failure),
_ => None,
}
}
}
#[derive(Debug, PartialEq)]
pub struct Call<'src> {
pub func: Function,
pub args: Vec<SpannedExpr<'src>>,
}
impl<'src> Call<'src> {
pub(crate) fn new(func: &str, args: Vec<SpannedExpr<'src>>) -> Result<Self, Error> {
let func = Function::new(func).ok_or_else(|| Error::UnknownFunction(func.to_string()))?;
match func {
Function::Contains | Function::StartsWith | Function::EndsWith if args.len() != 2 => {
return Err(Error::Arity(func, "exactly 2 arguments"));
}
Function::ToJSON | Function::FromJSON if args.len() != 1 => {
return Err(Error::Arity(func, "exactly 1 argument"));
}
Function::Join if args.is_empty() || args.len() > 2 => {
return Err(Error::Arity(func, "1 or 2 arguments"));
}
Function::HashFiles if args.is_empty() => {
return Err(Error::Arity(func, "at least 1 argument"));
}
Function::Case => {
if args.len() < 3 {
return Err(Error::Arity(func, "at least 3 arguments"));
}
if args.len().is_multiple_of(2) {
return Err(Error::Arity(func, "odd number of arguments"));
}
}
Function::Success | Function::Always | Function::Cancelled | Function::Failure
if !args.is_empty() =>
{
return Err(Error::Arity(func, "no arguments"));
}
_ => (),
}
Ok(Self { func, args })
}
pub(crate) fn consteval(&self) -> Option<Evaluation> {
let args = self
.args
.iter()
.map(|arg| arg.consteval())
.collect::<Option<Vec<Evaluation>>>()?;
match &self.func {
Function::Format => Self::consteval_format(&args),
Function::Contains => Self::consteval_contains(&args),
Function::StartsWith => Self::consteval_startswith(&args),
Function::EndsWith => Self::consteval_endswith(&args),
Function::ToJSON => Self::consteval_tojson(&args),
Function::FromJSON => Self::consteval_fromjson(&args),
Function::Join => Self::consteval_join(&args),
Function::Case => Self::consteval_case(&args),
_ => None,
}
}
fn consteval_format(args: &[Evaluation]) -> Option<Evaluation> {
if args.is_empty() {
return None;
}
let template = args[0].sema().to_string();
let mut result = String::new();
let mut index = 0;
while index < template.len() {
let lbrace = template[index..].find('{').map(|pos| index + pos);
let rbrace = template[index..].find('}').map(|pos| index + pos);
#[allow(clippy::unwrap_used)]
if let Some(lbrace_pos) = lbrace
&& (rbrace.is_none() || rbrace.unwrap() > lbrace_pos)
{
if template.as_bytes().get(lbrace_pos + 1) == Some(&b'{') {
result.push_str(&template[index..=lbrace_pos]);
index = lbrace_pos + 2;
continue;
}
if let Some(rbrace_pos) = rbrace
&& rbrace_pos > lbrace_pos + 1
&& let Some(arg_index) = Self::read_arg_index(&template, lbrace_pos + 1)
{
if 1 + arg_index > args.len() - 1 {
return None;
}
if lbrace_pos > index {
result.push_str(&template[index..lbrace_pos]);
}
result.push_str(&args[1 + arg_index].sema().to_string());
index = rbrace_pos + 1;
continue;
}
return None;
}
if let Some(rbrace_pos) = rbrace {
#[allow(clippy::unwrap_used)]
if lbrace.is_none() || lbrace.unwrap() > rbrace_pos {
if template.as_bytes().get(rbrace_pos + 1) == Some(&b'}') {
result.push_str(&template[index..=rbrace_pos]);
index = rbrace_pos + 2;
} else {
return None;
}
}
} else {
result.push_str(&template[index..]);
break;
}
}
Some(Evaluation::String(result))
}
fn read_arg_index(string: &str, start_index: usize) -> Option<usize> {
let mut length = 0;
let chars: Vec<char> = string.chars().collect();
while start_index + length < chars.len() {
let next_char = chars[start_index + length];
if next_char.is_ascii_digit() {
length += 1;
} else {
break;
}
}
if length < 1 {
return None;
}
let number_str: String = chars[start_index..start_index + length].iter().collect();
number_str.parse::<usize>().ok()
}
fn consteval_contains(args: &[Evaluation]) -> Option<Evaluation> {
if args.len() != 2 {
return None;
}
let search = &args[0];
let item = &args[1];
match search {
Evaluation::String(_)
| Evaluation::Number(_)
| Evaluation::Boolean(_)
| Evaluation::Null => {
let search_str = search.sema().to_string().to_uppercase();
let item_str = item.sema().to_string().to_uppercase();
Some(Evaluation::Boolean(search_str.contains(&item_str)))
}
Evaluation::Array(arr) => {
if arr.iter().any(|element| element.sema() == item.sema()) {
Some(Evaluation::Boolean(true))
} else {
Some(Evaluation::Boolean(false))
}
}
Evaluation::Object(_) => None,
}
}
fn consteval_startswith(args: &[Evaluation]) -> Option<Evaluation> {
if args.len() != 2 {
return None;
}
let search_string = &args[0];
let search_value = &args[1];
match (search_string, search_value) {
(
Evaluation::String(_)
| Evaluation::Number(_)
| Evaluation::Boolean(_)
| Evaluation::Null,
Evaluation::String(_)
| Evaluation::Number(_)
| Evaluation::Boolean(_)
| Evaluation::Null,
) => {
let haystack = EvaluationSema::upper_special(&search_string.sema().to_string());
let needle = EvaluationSema::upper_special(&search_value.sema().to_string());
Some(Evaluation::Boolean(haystack.starts_with(&needle)))
}
_ => Some(Evaluation::Boolean(false)),
}
}
fn consteval_endswith(args: &[Evaluation]) -> Option<Evaluation> {
if args.len() != 2 {
return None;
}
let search_string = &args[0];
let search_value = &args[1];
match (search_string, search_value) {
(
Evaluation::String(_)
| Evaluation::Number(_)
| Evaluation::Boolean(_)
| Evaluation::Null,
Evaluation::String(_)
| Evaluation::Number(_)
| Evaluation::Boolean(_)
| Evaluation::Null,
) => {
let haystack = EvaluationSema::upper_special(&search_string.sema().to_string());
let needle = EvaluationSema::upper_special(&search_value.sema().to_string());
Some(Evaluation::Boolean(haystack.ends_with(&needle)))
}
_ => Some(Evaluation::Boolean(false)),
}
}
fn consteval_tojson(args: &[Evaluation]) -> Option<Evaluation> {
if args.len() != 1 {
return None;
}
let value = &args[0];
let json_value: serde_json::Value = value.clone().try_into().ok()?;
let json_str = serde_json::to_string_pretty(&json_value).ok()?;
Some(Evaluation::String(json_str))
}
fn consteval_fromjson(args: &[Evaluation]) -> Option<Evaluation> {
if args.len() != 1 {
return None;
}
let json_str = args[0].sema().to_string();
if json_str.trim().is_empty() {
return None;
}
serde_json::from_str::<serde_json::Value>(&json_str)
.ok()?
.try_into()
.ok()
}
fn consteval_join(args: &[Evaluation]) -> Option<Evaluation> {
if args.is_empty() || args.len() > 2 {
return None;
}
let array_or_string = &args[0];
let separator = if args.len() > 1 {
args[1].sema().to_string()
} else {
",".to_string()
};
match array_or_string {
Evaluation::String(_)
| Evaluation::Number(_)
| Evaluation::Boolean(_)
| Evaluation::Null => Some(Evaluation::String(array_or_string.sema().to_string())),
Evaluation::Array(arr) => {
let joined = arr
.iter()
.map(|item| item.sema().to_string())
.collect::<Vec<String>>()
.join(&separator);
Some(Evaluation::String(joined))
}
Evaluation::Object(_) => Some(Evaluation::String("".to_string())),
}
}
fn consteval_case(args: &[Evaluation]) -> Option<Evaluation> {
if args.len() < 3 || args.len().is_multiple_of(2) {
return None;
}
let (pairs, default) = args.as_chunks::<2>();
for pair in pairs {
let pred = &pair[0];
let val = &pair[1];
match pred {
Evaluation::Boolean(true) => return Some(val.clone()),
Evaluation::Boolean(false) => continue,
_ => return None,
}
}
Some(default[0].clone())
}
}
#[cfg(test)]
mod tests {
use crate::{
Error, Expr,
call::{Call, Function},
};
#[test]
fn test_function_new() {
assert_eq!(Function::new("contains"), Some(Function::Contains));
assert_eq!(Function::new("STARTSWITH"), Some(Function::StartsWith));
assert_eq!(Function::new("endsWith"), Some(Function::EndsWith));
assert_eq!(Function::new("unknown"), None);
}
#[test]
fn test_consteval_fromjson() -> Result<(), Error> {
use crate::Evaluation;
let test_cases = &[
("fromJSON('null')", Evaluation::Null),
("fromJSON('true')", Evaluation::Boolean(true)),
("fromJSON('false')", Evaluation::Boolean(false)),
("fromJSON('42')", Evaluation::Number(42.0)),
("fromJSON('3.14')", Evaluation::Number(3.14)),
("fromJSON('-0')", Evaluation::Number(0.0)),
("fromJSON('0')", Evaluation::Number(0.0)),
(
"fromJSON('\"hello\"')",
Evaluation::String("hello".to_string()),
),
("fromJSON('\"\"')", Evaluation::String("".to_string())),
("fromJSON('[]')", Evaluation::Array(vec![])),
(
"fromJSON('[1, 2, 3]')",
Evaluation::Array(vec![
Evaluation::Number(1.0),
Evaluation::Number(2.0),
Evaluation::Number(3.0),
]),
),
(
"fromJSON('[\"a\", \"b\", null, true, 123]')",
Evaluation::Array(vec![
Evaluation::String("a".to_string()),
Evaluation::String("b".to_string()),
Evaluation::Null,
Evaluation::Boolean(true),
Evaluation::Number(123.0),
]),
),
(
"fromJSON('{}')",
Evaluation::Object(std::collections::HashMap::new()),
),
(
"fromJSON('{\"key\": \"value\"}')",
Evaluation::Object({
let mut map = std::collections::HashMap::new();
map.insert("key".to_string(), Evaluation::String("value".to_string()));
map
}),
),
(
"fromJSON('{\"num\": 42, \"bool\": true, \"null\": null}')",
Evaluation::Object({
let mut map = std::collections::HashMap::new();
map.insert("num".to_string(), Evaluation::Number(42.0));
map.insert("bool".to_string(), Evaluation::Boolean(true));
map.insert("null".to_string(), Evaluation::Null);
map
}),
),
(
"fromJSON('{\"array\": [1, 2], \"object\": {\"nested\": true}}')",
Evaluation::Object({
let mut map = std::collections::HashMap::new();
map.insert(
"array".to_string(),
Evaluation::Array(vec![Evaluation::Number(1.0), Evaluation::Number(2.0)]),
);
let mut nested_map = std::collections::HashMap::new();
nested_map.insert("nested".to_string(), Evaluation::Boolean(true));
map.insert("object".to_string(), Evaluation::Object(nested_map));
map
}),
),
];
for (expr_str, expected) in test_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval().unwrap();
assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
}
Ok(())
}
#[test]
fn test_consteval_fromjson_error_cases() -> Result<(), Error> {
let error_cases = &[
"fromJSON('')", "fromJSON(' ')", "fromJSON('invalid')", "fromJSON('{invalid}')", "fromJSON('[1, 2,]')", ];
for expr_str in error_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval();
assert!(
result.is_none(),
"Expected None for invalid JSON: {}",
expr_str
);
}
Ok(())
}
#[test]
fn test_consteval_fromjson_display_format() -> Result<(), Error> {
use crate::Evaluation;
let test_cases = &[
(Evaluation::Array(vec![Evaluation::Number(1.0)]), "Array"),
(
Evaluation::Object(std::collections::HashMap::new()),
"Object",
),
];
for (result, expected) in test_cases {
assert_eq!(result.sema().to_string(), *expected);
}
Ok(())
}
#[test]
fn test_consteval_tojson_fromjson_roundtrip() -> Result<(), Error> {
use crate::Evaluation;
let test_cases = &[
"[1, 2, 3]",
r#"{"key": "value"}"#,
r#"[1, "hello", true, null]"#,
r#"{"array": [1, 2], "object": {"nested": true}}"#,
];
for json_str in test_cases {
let from_expr_str = format!("fromJSON('{}')", json_str);
let from_expr = Expr::parse(&from_expr_str)?;
let parsed = from_expr.consteval().unwrap();
let to_result = Call::consteval_tojson(&[parsed.clone()]).unwrap();
let reparsed_expr_str = format!("fromJSON('{}')", to_result.sema().to_string());
let reparsed_expr = Expr::parse(&reparsed_expr_str)?;
let reparsed = reparsed_expr.consteval().unwrap();
match (&parsed, &reparsed) {
(Evaluation::Array(a), Evaluation::Array(b)) => assert_eq!(a, b),
(Evaluation::Object(_), Evaluation::Object(_)) => {
assert!(matches!(parsed, Evaluation::Object(_)));
assert!(matches!(reparsed, Evaluation::Object(_)));
}
(a, b) => assert_eq!(a, b),
}
}
Ok(())
}
#[test]
fn test_consteval_format() -> Result<(), Error> {
use crate::Evaluation;
let test_cases = &[
(
"format('Hello {0}', 'world')",
Evaluation::String("Hello world".to_string()),
),
(
"format('{0} {1}', 'Hello', 'world')",
Evaluation::String("Hello world".to_string()),
),
(
"format('Value: {0}', 42)",
Evaluation::String("Value: 42".to_string()),
),
(
"format('{{0}}', 'test')",
Evaluation::String("{0}".to_string()),
),
(
"format('{{Hello}} {0}', 'world')",
Evaluation::String("{Hello} world".to_string()),
),
(
"format('{0} {{1}}', 'Hello')",
Evaluation::String("Hello {1}".to_string()),
),
(
"format('}}{{', 'test')",
Evaluation::String("}{".to_string()),
),
(
"format('{{{{}}}}', 'test')",
Evaluation::String("{{}}".to_string()),
),
(
"format('{0} {1} {2}', 'a', 'b', 'c')",
Evaluation::String("a b c".to_string()),
),
(
"format('{2} {1} {0}', 'a', 'b', 'c')",
Evaluation::String("c b a".to_string()),
),
(
"format('{0} {0} {0}', 'test')",
Evaluation::String("test test test".to_string()),
),
(
"format('Hello world')",
Evaluation::String("Hello world".to_string()),
),
("format('abc {{')", Evaluation::String("abc {".to_string())),
("format('abc }}')", Evaluation::String("abc }".to_string())),
(
"format('abc {{}}')",
Evaluation::String("abc {}".to_string()),
),
];
for (expr_str, expected) in test_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval().unwrap();
assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
}
Ok(())
}
#[test]
fn test_consteval_format_error_cases() -> Result<(), Error> {
let error_cases = &[
"format('{0', 'test')", "format('0}', 'test')", "format('{a}', 'test')", "format('{1}', 'test')", "format('{0} {2}', 'a', 'b')", "format('{}', 'test')", "format('{-1}', 'test')", ];
for expr_str in error_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval();
assert!(
result.is_none(),
"Expected None for invalid format string: {}",
expr_str
);
}
Ok(())
}
#[test]
fn test_consteval_contains() -> Result<(), Error> {
use crate::Evaluation;
let test_cases = &[
(
"contains('hello world', 'world')",
Evaluation::Boolean(true),
),
(
"contains('hello world', 'WORLD')",
Evaluation::Boolean(true),
),
(
"contains('HELLO WORLD', 'world')",
Evaluation::Boolean(true),
),
("contains('hello world', 'foo')", Evaluation::Boolean(false)),
("contains('test', '')", Evaluation::Boolean(true)),
("contains('123', '2')", Evaluation::Boolean(true)),
("contains(123, '2')", Evaluation::Boolean(true)),
("contains('hello123', 123)", Evaluation::Boolean(true)),
("contains('true', true)", Evaluation::Boolean(true)),
("contains('false', false)", Evaluation::Boolean(true)),
("contains('null', null)", Evaluation::Boolean(true)),
("contains(null, '')", Evaluation::Boolean(true)),
(
"contains(fromJSON('[1, 2, 3]'), 2)",
Evaluation::Boolean(true),
),
(
"contains(fromJSON('[1, 2, 3]'), 4)",
Evaluation::Boolean(false),
),
(
"contains(fromJSON('[\"a\", \"b\", \"c\"]'), 'b')",
Evaluation::Boolean(true),
),
(
"contains(fromJSON('[\"a\", \"b\", \"c\"]'), 'B')",
Evaluation::Boolean(true), ),
(
"contains(fromJSON('[true, false, null]'), true)",
Evaluation::Boolean(true),
),
(
"contains(fromJSON('[true, false, null]'), null)",
Evaluation::Boolean(true),
),
(
"contains(fromJSON('[]'), 'anything')",
Evaluation::Boolean(false),
),
(
"contains(fromJSON('[1, \"hello\", true, null]'), 'hello')",
Evaluation::Boolean(true),
),
(
"contains(fromJSON('[1, \"hello\", true, null]'), 1)",
Evaluation::Boolean(true),
),
];
for (expr_str, expected) in test_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval().unwrap();
assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
}
Ok(())
}
#[test]
fn test_consteval_join() -> Result<(), Error> {
use crate::Evaluation;
let test_cases = &[
(
"join(fromJSON('[\"a\", \"b\", \"c\"]'))",
Evaluation::String("a,b,c".to_string()),
),
(
"join(fromJSON('[1, 2, 3]'))",
Evaluation::String("1,2,3".to_string()),
),
(
"join(fromJSON('[true, false, null]'))",
Evaluation::String("true,false,".to_string()),
),
(
"join(fromJSON('[\"a\", \"b\", \"c\"]'), ' ')",
Evaluation::String("a b c".to_string()),
),
(
"join(fromJSON('[1, 2, 3]'), '-')",
Evaluation::String("1-2-3".to_string()),
),
(
"join(fromJSON('[\"hello\", \"world\"]'), ' | ')",
Evaluation::String("hello | world".to_string()),
),
(
"join(fromJSON('[\"a\", \"b\", \"c\"]'), '')",
Evaluation::String("abc".to_string()),
),
("join(fromJSON('[]'))", Evaluation::String("".to_string())),
(
"join(fromJSON('[]'), '-')",
Evaluation::String("".to_string()),
),
(
"join(fromJSON('[\"single\"]'))",
Evaluation::String("single".to_string()),
),
(
"join(fromJSON('[\"single\"]'), '-')",
Evaluation::String("single".to_string()),
),
("join('hello')", Evaluation::String("hello".to_string())),
(
"join('hello', '-')",
Evaluation::String("hello".to_string()),
),
("join(123)", Evaluation::String("123".to_string())),
("join(true)", Evaluation::String("true".to_string())),
("join(null)", Evaluation::String("".to_string())),
(
"join(fromJSON('[1, \"hello\", true, null]'))",
Evaluation::String("1,hello,true,".to_string()),
),
(
"join(fromJSON('[1, \"hello\", true, null]'), ' | ')",
Evaluation::String("1 | hello | true | ".to_string()),
),
(
"join(fromJSON('[\"a\", \"b\", \"c\"]'), 123)",
Evaluation::String("a123b123c".to_string()),
),
(
"join(fromJSON('[\"a\", \"b\", \"c\"]'), true)",
Evaluation::String("atruebtruec".to_string()),
),
];
for (expr_str, expected) in test_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval().unwrap();
assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
}
Ok(())
}
#[test]
fn test_consteval_endswith() -> Result<(), Error> {
use crate::Evaluation;
let test_cases = &[
(
"endsWith('hello world', 'world')",
Evaluation::Boolean(true),
),
(
"endsWith('hello world', 'WORLD')",
Evaluation::Boolean(true),
),
(
"endsWith('HELLO WORLD', 'world')",
Evaluation::Boolean(true),
),
(
"endsWith('hello world', 'hello')",
Evaluation::Boolean(false),
),
("endsWith('hello world', 'foo')", Evaluation::Boolean(false)),
("endsWith('test', '')", Evaluation::Boolean(true)),
("endsWith('', '')", Evaluation::Boolean(true)),
("endsWith('', 'test')", Evaluation::Boolean(false)),
("endsWith('123', '3')", Evaluation::Boolean(true)),
("endsWith(123, '3')", Evaluation::Boolean(true)),
("endsWith('hello123', 123)", Evaluation::Boolean(true)),
("endsWith(12345, 345)", Evaluation::Boolean(true)),
("endsWith('test true', true)", Evaluation::Boolean(true)),
("endsWith('test false', false)", Evaluation::Boolean(true)),
("endsWith(true, 'ue')", Evaluation::Boolean(true)),
("endsWith('test null', null)", Evaluation::Boolean(true)),
("endsWith(null, '')", Evaluation::Boolean(true)),
("endsWith('something', null)", Evaluation::Boolean(true)), (
"endsWith(fromJSON('[1, 2, 3]'), '3')",
Evaluation::Boolean(false),
),
(
"endsWith('test', fromJSON('[1, 2, 3]'))",
Evaluation::Boolean(false),
),
(
"endsWith(fromJSON('{\"key\": \"value\"}'), 'value')",
Evaluation::Boolean(false),
),
(
"endsWith('test', fromJSON('{\"key\": \"value\"}'))",
Evaluation::Boolean(false),
),
(
"endsWith('TestString', 'STRING')",
Evaluation::Boolean(true),
),
("endsWith('CamelCase', 'case')", Evaluation::Boolean(true)),
("endsWith('exact', 'exact')", Evaluation::Boolean(true)),
(
"endsWith('short', 'very long suffix')",
Evaluation::Boolean(false),
),
];
for (expr_str, expected) in test_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval().unwrap();
assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
}
Ok(())
}
#[test]
fn test_consteval_startswith() -> Result<(), Error> {
use crate::Evaluation;
let test_cases = &[
(
"startsWith('hello world', 'hello')",
Evaluation::Boolean(true),
),
(
"startsWith('hello world', 'HELLO')",
Evaluation::Boolean(true),
),
(
"startsWith('HELLO WORLD', 'hello')",
Evaluation::Boolean(true),
),
(
"startsWith('hello world', 'world')",
Evaluation::Boolean(false),
),
(
"startsWith('hello world', 'foo')",
Evaluation::Boolean(false),
),
("startsWith('test', '')", Evaluation::Boolean(true)),
("startsWith('', '')", Evaluation::Boolean(true)),
("startsWith('', 'test')", Evaluation::Boolean(false)),
("startsWith('123', '1')", Evaluation::Boolean(true)),
("startsWith(123, '1')", Evaluation::Boolean(true)),
("startsWith('123hello', 123)", Evaluation::Boolean(true)),
("startsWith(12345, 123)", Evaluation::Boolean(true)),
("startsWith('true test', true)", Evaluation::Boolean(true)),
("startsWith('false test', false)", Evaluation::Boolean(true)),
("startsWith(true, 'tr')", Evaluation::Boolean(true)),
("startsWith('null test', null)", Evaluation::Boolean(true)),
("startsWith(null, '')", Evaluation::Boolean(true)),
(
"startsWith('something', null)",
Evaluation::Boolean(true), ),
(
"startsWith(fromJSON('[1, 2, 3]'), '1')",
Evaluation::Boolean(false),
),
(
"startsWith('test', fromJSON('[1, 2, 3]'))",
Evaluation::Boolean(false),
),
(
"startsWith(fromJSON('{\"key\": \"value\"}'), 'key')",
Evaluation::Boolean(false),
),
(
"startsWith('test', fromJSON('{\"key\": \"value\"}'))",
Evaluation::Boolean(false),
),
(
"startsWith('TestString', 'TEST')",
Evaluation::Boolean(true),
),
(
"startsWith('CamelCase', 'camel')",
Evaluation::Boolean(true),
),
("startsWith('exact', 'exact')", Evaluation::Boolean(true)),
(
"startsWith('short', 'very long prefix')",
Evaluation::Boolean(false),
),
(
"startsWith('prefix_suffix', 'prefix')",
Evaluation::Boolean(true),
),
(
"startsWith('prefix_suffix', 'suffix')",
Evaluation::Boolean(false),
),
];
for (expr_str, expected) in test_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval().unwrap();
assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
}
Ok(())
}
#[test]
fn test_consteval_case() -> Result<(), Error> {
use crate::Evaluation;
let test_cases = &[
(
"case(true, 'yes', false, 'no', 'maybe')",
Some(Evaluation::String("yes".to_string())),
),
(
"case(false, 'yes', true, 'no', 'maybe')",
Some(Evaluation::String("no".to_string())),
),
(
"case(false, 'first', false, 'second', true, 'third', 'default')",
Some(Evaluation::String("third".to_string())),
),
(
"case(false, 'first', false, 'second', false, 'third', 'default')",
Some(Evaluation::String("default".to_string())),
),
("case(1, 'yes', 0, 'no', 'maybe')", None),
];
for (expr_str, expected) in test_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval();
assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
}
Ok(())
}
#[test]
fn test_evaluate_constant_functions() -> Result<(), Error> {
use crate::Evaluation;
let test_cases = &[
(
"format('{0}', 'hello')",
Evaluation::String("hello".to_string()),
),
(
"format('{0} {1}', 'hello', 'world')",
Evaluation::String("hello world".to_string()),
),
(
"format('Value: {0}', 42)",
Evaluation::String("Value: 42".to_string()),
),
(
"contains('hello world', 'world')",
Evaluation::Boolean(true),
),
("contains('hello world', 'foo')", Evaluation::Boolean(false)),
("contains('test', '')", Evaluation::Boolean(true)),
(
"startsWith('hello world', 'hello')",
Evaluation::Boolean(true),
),
(
"startsWith('hello world', 'world')",
Evaluation::Boolean(false),
),
("startsWith('test', '')", Evaluation::Boolean(true)),
(
"endsWith('hello world', 'world')",
Evaluation::Boolean(true),
),
(
"endsWith('hello world', 'hello')",
Evaluation::Boolean(false),
),
("endsWith('test', '')", Evaluation::Boolean(true)),
(
"toJSON('hello')",
Evaluation::String("\"hello\"".to_string()),
),
("toJSON(42)", Evaluation::String("42".to_string())),
("toJSON(true)", Evaluation::String("true".to_string())),
("toJSON(null)", Evaluation::String("null".to_string())),
(
"fromJSON('\"hello\"')",
Evaluation::String("hello".to_string()),
),
("fromJSON('42')", Evaluation::Number(42.0)),
("fromJSON('true')", Evaluation::Boolean(true)),
("fromJSON('null')", Evaluation::Null),
(
"fromJSON('[1, 2, 3]')",
Evaluation::Array(vec![
Evaluation::Number(1.0),
Evaluation::Number(2.0),
Evaluation::Number(3.0),
]),
),
(
"fromJSON('{\"key\": \"value\"}')",
Evaluation::Object({
let mut map = std::collections::HashMap::new();
map.insert("key".to_string(), Evaluation::String("value".to_string()));
map
}),
),
];
for (expr_str, expected) in test_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval().unwrap();
assert_eq!(
result, *expected,
"Failed for expression: {} {result:?}",
expr_str
);
}
Ok(())
}
}