use cel::{
Context, ExecutionError, ResolveResult,
extractors::{Arguments, This},
objects::Value,
};
use std::sync::Arc;
pub fn register(ctx: &mut Context<'_>) {
ctx.add_function("charAt", char_at);
ctx.add_function("lowerAscii", lower_ascii);
ctx.add_function("upperAscii", upper_ascii);
ctx.add_function("replace", string_replace);
ctx.add_function("split", string_split);
ctx.add_function("substring", substring);
ctx.add_function("trim", trim);
ctx.add_function("join", join);
ctx.add_function("strings.quote", strings_quote);
}
fn char_at(This(this): This<Arc<String>>, idx: i64) -> ResolveResult {
let chars: Vec<char> = this.chars().collect();
if idx < 0 || idx as usize > chars.len() {
return Err(ExecutionError::function_error(
"charAt",
format!("index {idx} out of range for string of length {}", chars.len()),
));
}
if idx as usize == chars.len() {
return Ok(Value::String(Arc::new(String::new())));
}
Ok(Value::String(Arc::new(chars[idx as usize].to_string())))
}
pub(crate) fn string_index_of(This(this): This<Arc<String>>, Arguments(args): Arguments) -> ResolveResult {
let search = match args.first() {
Some(Value::String(s)) => s.clone(),
_ => {
return Err(ExecutionError::function_error(
"indexOf",
"expected string argument",
));
}
};
let offset: usize = match args.get(1) {
Some(Value::Int(n)) => (*n).max(0) as usize,
_ => 0,
};
let chars: Vec<char> = this.chars().collect();
let search_chars: Vec<char> = search.chars().collect();
if search_chars.is_empty() {
return Ok(Value::Int(offset as i64));
}
for i in offset..chars.len() {
if i + search_chars.len() <= chars.len() && chars[i..i + search_chars.len()] == search_chars[..] {
return Ok(Value::Int(i as i64));
}
}
Ok(Value::Int(-1))
}
pub(crate) fn string_last_index_of(
This(this): This<Arc<String>>,
Arguments(args): Arguments,
) -> ResolveResult {
let search = match args.first() {
Some(Value::String(s)) => s.clone(),
_ => {
return Err(ExecutionError::function_error(
"lastIndexOf",
"expected string argument",
));
}
};
let chars: Vec<char> = this.chars().collect();
let search_chars: Vec<char> = search.chars().collect();
let end: usize = match args.get(1) {
Some(Value::Int(n)) => ((*n).max(0) as usize).min(chars.len()),
_ => chars.len(),
};
if search_chars.is_empty() {
return Ok(Value::Int(end as i64));
}
let mut result: i64 = -1;
for i in 0..end {
if i + search_chars.len() <= end && chars[i..i + search_chars.len()] == search_chars[..] {
result = i as i64;
}
}
Ok(Value::Int(result))
}
fn lower_ascii(This(this): This<Arc<String>>) -> ResolveResult {
Ok(Value::String(Arc::new(this.to_ascii_lowercase())))
}
fn upper_ascii(This(this): This<Arc<String>>) -> ResolveResult {
Ok(Value::String(Arc::new(this.to_ascii_uppercase())))
}
fn string_replace(This(this): This<Arc<String>>, Arguments(args): Arguments) -> ResolveResult {
let from = match args.first() {
Some(Value::String(s)) => s.clone(),
_ => {
return Err(ExecutionError::function_error(
"replace",
"expected string argument",
));
}
};
let to = match args.get(1) {
Some(Value::String(s)) => s.clone(),
_ => {
return Err(ExecutionError::function_error(
"replace",
"expected string argument",
));
}
};
let result = match args.get(2) {
Some(Value::Int(n)) => this.replacen(from.as_str(), to.as_str(), (*n).max(0) as usize),
_ => this.replace(from.as_str(), to.as_str()),
};
Ok(Value::String(Arc::new(result)))
}
fn string_split(This(this): This<Arc<String>>, Arguments(args): Arguments) -> ResolveResult {
let separator = match args.first() {
Some(Value::String(s)) => s.clone(),
_ => {
return Err(ExecutionError::function_error(
"split",
"expected string argument",
));
}
};
let parts: Vec<Value> = match args.get(1) {
Some(Value::Int(n)) if *n == 0 => vec![],
Some(Value::Int(n)) if *n < 0 => this
.split(separator.as_str())
.map(|s| Value::String(Arc::new(s.to_string())))
.collect(),
Some(Value::Int(n)) => this
.splitn(*n as usize, separator.as_str())
.map(|s| Value::String(Arc::new(s.to_string())))
.collect(),
_ => this
.split(separator.as_str())
.map(|s| Value::String(Arc::new(s.to_string())))
.collect(),
};
Ok(Value::List(Arc::new(parts)))
}
fn substring(This(this): This<Arc<String>>, Arguments(args): Arguments) -> ResolveResult {
let start = match args.first() {
Some(Value::Int(n)) => *n,
_ => {
return Err(ExecutionError::function_error(
"substring",
"expected int argument",
));
}
};
let chars: Vec<char> = this.chars().collect();
let len = chars.len();
if start < 0 || start as usize > len {
return Err(ExecutionError::function_error(
"substring",
format!("start index {start} out of range for string of length {len}"),
));
}
let end = match args.get(1) {
Some(Value::Int(n)) => {
if *n < start || *n as usize > len {
return Err(ExecutionError::function_error(
"substring",
format!("end index {n} out of range"),
));
}
*n as usize
}
_ => len,
};
let result: String = chars[start as usize..end].iter().collect();
Ok(Value::String(Arc::new(result)))
}
fn trim(This(this): This<Arc<String>>) -> ResolveResult {
Ok(Value::String(Arc::new(this.trim().to_string())))
}
fn join(This(this): This<Arc<Vec<Value>>>, Arguments(args): Arguments) -> ResolveResult {
let separator = match args.first() {
Some(Value::String(s)) => s.to_string(),
_ => String::new(),
};
let parts: Vec<String> = this
.iter()
.map(|v| match v {
Value::String(s) => s.to_string(),
other => format!("{other:?}"),
})
.collect();
Ok(Value::String(Arc::new(parts.join(&separator))))
}
pub(crate) fn string_reverse(This(this): This<Arc<String>>) -> ResolveResult {
let reversed: String = this.chars().rev().collect();
Ok(Value::String(Arc::new(reversed)))
}
fn strings_quote(s: Arc<String>) -> ResolveResult {
let mut escaped = String::with_capacity(s.len() + 2);
escaped.push('"');
for ch in s.chars() {
match ch {
'\\' => escaped.push_str("\\\\"),
'"' => escaped.push_str("\\\""),
'\x07' => escaped.push_str("\\a"),
'\x08' => escaped.push_str("\\b"),
'\x0C' => escaped.push_str("\\f"),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
'\x0B' => escaped.push_str("\\v"),
c => escaped.push(c),
}
}
escaped.push('"');
Ok(Value::String(Arc::new(escaped)))
}
#[cfg(test)]
mod tests {
use super::*;
use cel::Program;
fn eval(expr: &str) -> Value {
let mut ctx = Context::default();
register(&mut ctx);
crate::dispatch::register(&mut ctx);
Program::compile(expr).unwrap().execute(&ctx).unwrap()
}
#[test]
fn test_char_at() {
assert_eq!(eval("'hello'.charAt(0)"), Value::String(Arc::new("h".into())));
assert_eq!(eval("'hello'.charAt(4)"), Value::String(Arc::new("o".into())));
}
#[test]
fn test_index_of() {
assert_eq!(eval("'hello world'.indexOf('world')"), Value::Int(6));
assert_eq!(eval("'hello'.indexOf('x')"), Value::Int(-1));
assert_eq!(eval("'hello'.indexOf('')"), Value::Int(0));
}
#[test]
fn test_last_index_of() {
assert_eq!(eval("'abcabc'.lastIndexOf('abc')"), Value::Int(3));
assert_eq!(eval("'hello'.lastIndexOf('x')"), Value::Int(-1));
}
#[test]
fn test_lower_upper_ascii() {
assert_eq!(
eval("'Hello World'.lowerAscii()"),
Value::String(Arc::new("hello world".into()))
);
assert_eq!(
eval("'Hello World'.upperAscii()"),
Value::String(Arc::new("HELLO WORLD".into()))
);
}
#[test]
fn test_trim() {
assert_eq!(
eval("' hello '.trim()"),
Value::String(Arc::new("hello".into()))
);
}
#[test]
fn test_split() {
assert_eq!(
eval("'a,b,c'.split(',')"),
Value::List(Arc::new(vec![
Value::String(Arc::new("a".into())),
Value::String(Arc::new("b".into())),
Value::String(Arc::new("c".into())),
]))
);
}
#[test]
fn test_join() {
assert_eq!(
eval("['a', 'b', 'c'].join('-')"),
Value::String(Arc::new("a-b-c".into()))
);
}
#[test]
fn test_replace() {
assert_eq!(
eval("'hello world'.replace('world', 'CEL')"),
Value::String(Arc::new("hello CEL".into()))
);
}
#[test]
fn test_substring() {
assert_eq!(
eval("'hello'.substring(1)"),
Value::String(Arc::new("ello".into()))
);
}
#[test]
fn test_strings_quote() {
assert_eq!(
eval("strings.quote('hello')"),
Value::String(Arc::new("\"hello\"".into()))
);
}
fn eval_err(expr: &str) -> cel::ExecutionError {
let mut ctx = Context::default();
register(&mut ctx);
crate::dispatch::register(&mut ctx);
Program::compile(expr).unwrap().execute(&ctx).unwrap_err()
}
#[test]
fn test_char_at_out_of_bounds() {
eval_err("'hello'.charAt(-1)");
eval_err("'hello'.charAt(6)");
}
#[test]
fn test_char_at_at_length() {
assert_eq!(eval("'hello'.charAt(5)"), Value::String(Arc::new("".into())));
assert_eq!(eval("'tacocat'.charAt(7)"), Value::String(Arc::new("".into())));
}
#[test]
fn test_char_at_unicode() {
assert_eq!(eval("'héllo'.charAt(1)"), Value::String(Arc::new("é".into())));
}
#[test]
fn test_index_of_with_offset() {
assert_eq!(eval("'abcabc'.indexOf('abc', 1)"), Value::Int(3));
assert_eq!(eval("'hello'.indexOf('h', -5)"), Value::Int(0));
assert_eq!(eval("'hello'.indexOf('h', 100)"), Value::Int(-1));
}
#[test]
fn test_last_index_of_with_offset() {
assert_eq!(eval("'abcabc'.lastIndexOf('abc', 3)"), Value::Int(0));
assert_eq!(eval("'hello'.lastIndexOf('', 3)"), Value::Int(3));
}
#[test]
fn test_substring_two_args() {
assert_eq!(
eval("'hello'.substring(1, 3)"),
Value::String(Arc::new("el".into()))
);
}
#[test]
fn test_substring_errors() {
eval_err("'hello'.substring(-1)");
eval_err("'hello'.substring(10)");
eval_err("'hello'.substring(3, 2)"); eval_err("'hello'.substring(0, 10)"); }
#[test]
fn test_replace_with_count() {
assert_eq!(
eval("'aaa'.replace('a', 'b', 2)"),
Value::String(Arc::new("bba".into()))
);
assert_eq!(
eval("'aaa'.replace('a', 'b', 0)"),
Value::String(Arc::new("aaa".into()))
);
}
#[test]
fn test_split_with_limit() {
assert_eq!(
eval("'a,b,c'.split(',', 2)"),
Value::List(Arc::new(vec![
Value::String(Arc::new("a".into())),
Value::String(Arc::new("b,c".into())),
]))
);
}
#[test]
fn test_join_no_separator() {
assert_eq!(
eval("['a', 'b', 'c'].join()"),
Value::String(Arc::new("abc".into()))
);
}
#[test]
fn test_string_reverse() {
assert_eq!(eval("'hello'.reverse()"), Value::String(Arc::new("olleh".into())));
assert_eq!(eval("''.reverse()"), Value::String(Arc::new("".into())));
assert_eq!(eval("'a'.reverse()"), Value::String(Arc::new("a".into())));
}
#[test]
fn test_strings_quote_escapes() {
assert_eq!(
eval("strings.quote('a\\nb')"),
Value::String(Arc::new("\"a\\nb\"".into()))
);
assert_eq!(
eval("strings.quote('a\\tb')"),
Value::String(Arc::new("\"a\\tb\"".into()))
);
}
#[test]
fn test_char_at_unicode_multi() {
assert_eq!(eval("'©αT'.charAt(0)"), Value::String(Arc::new("©".into())));
assert_eq!(eval("'©αT'.charAt(1)"), Value::String(Arc::new("α".into())));
assert_eq!(eval("'©αT'.charAt(2)"), Value::String(Arc::new("T".into())));
}
#[test]
fn test_index_of_unicode() {
assert_eq!(eval("'ta©o©αT'.indexOf('©')"), Value::Int(2));
assert_eq!(eval("'ta©o©αT'.indexOf('©', 3)"), Value::Int(4));
assert_eq!(eval("'ta©o©αT'.indexOf('©αT', 3)"), Value::Int(4));
}
#[test]
fn test_index_of_full_match() {
assert_eq!(eval("'hello wello'.indexOf('hello wello')"), Value::Int(0));
}
#[test]
fn test_index_of_not_found_longer() {
assert_eq!(eval("'hello wello'.indexOf('elbo room!!!')"), Value::Int(-1));
}
#[test]
fn test_last_index_of_unicode() {
assert_eq!(eval("'ta©o©αT'.lastIndexOf('©')"), Value::Int(4));
assert_eq!(eval("'ta©o©αT'.lastIndexOf('©', 3)"), Value::Int(2));
}
#[test]
fn test_last_index_of_empty_string() {
assert_eq!(eval("''.lastIndexOf('@@')"), Value::Int(-1));
assert_eq!(eval("'tacocat'.lastIndexOf('')"), Value::Int(7));
}
#[test]
fn test_last_index_of_full_match() {
assert_eq!(eval("'hello wello'.lastIndexOf('hello wello')"), Value::Int(0));
}
#[test]
fn test_last_index_of_overlapping() {
assert_eq!(eval("'bananananana'.lastIndexOf('nana', 7)"), Value::Int(2));
assert_eq!(eval("'bananananana'.lastIndexOf('nana')"), Value::Int(8));
}
#[test]
fn test_replace_empty_pattern() {
assert_eq!(
eval("'hello hello'.replace('', '_')"),
Value::String(Arc::new("_h_e_l_l_o_ _h_e_l_l_o_".into()))
);
}
#[test]
fn test_split_limit_zero() {
assert_eq!(eval("'a,b,c'.split(',', 0)"), Value::List(Arc::new(vec![])));
}
#[test]
fn test_split_negative_limit() {
assert_eq!(
eval("'o©o©o©o'.split('©', -1)"),
Value::List(Arc::new(vec![
Value::String(Arc::new("o".into())),
Value::String(Arc::new("o".into())),
Value::String(Arc::new("o".into())),
Value::String(Arc::new("o".into())),
]))
);
}
#[test]
fn test_substring_unicode() {
assert_eq!(
eval("'ta©o©αT'.substring(2, 6)"),
Value::String(Arc::new("©o©α".into()))
);
}
#[test]
fn test_substring_at_end() {
assert_eq!(
eval("'ta©o©αT'.substring(7, 7)"),
Value::String(Arc::new("".into()))
);
}
#[test]
fn test_lower_ascii_non_ascii_preserved() {
assert_eq!(
eval("'TacoCÆt'.lowerAscii()"),
Value::String(Arc::new("tacocÆt".into()))
);
}
#[test]
fn test_upper_ascii_non_ascii_preserved() {
assert_eq!(
eval("'tacoCαt'.upperAscii()"),
Value::String(Arc::new("TACOCαT".into()))
);
}
#[test]
fn test_strings_quote_special_escapes() {
let result = strings_quote(Arc::new("\x07".into())).unwrap();
assert_eq!(result, Value::String(Arc::new("\"\\a\"".into())));
let result = strings_quote(Arc::new("\x08".into())).unwrap();
assert_eq!(result, Value::String(Arc::new("\"\\b\"".into())));
let result = strings_quote(Arc::new("\x0C".into())).unwrap();
assert_eq!(result, Value::String(Arc::new("\"\\f\"".into())));
let result = strings_quote(Arc::new("\x0B".into())).unwrap();
assert_eq!(result, Value::String(Arc::new("\"\\v\"".into())));
}
#[test]
fn test_strings_quote_unicode_passthrough() {
let result = strings_quote(Arc::new("завтра".into())).unwrap();
assert_eq!(result, Value::String(Arc::new("\"завтра\"".into())));
}
#[test]
fn test_strings_quote_embedded_quote() {
let result = strings_quote(Arc::new("mid string \" quote".into())).unwrap();
assert_eq!(
result,
Value::String(Arc::new("\"mid string \\\" quote\"".into()))
);
}
}