use std::rc::Rc;
use heck::{
ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase,
ToTrainCase, ToUpperCamelCase,
};
use std::collections::HashSet;
use crate::common::{
ArgumentType, Context, ErrorReason, Function, JmespathError, Rcvar, Runtime, Variable,
};
use crate::define_function;
use crate::register_if_enabled;
pub fn register(runtime: &mut Runtime) {
runtime.register_function("lower", Box::new(LowerFn::new()));
runtime.register_function("upper", Box::new(UpperFn::new()));
runtime.register_function("trim", Box::new(TrimFn::new()));
runtime.register_function("trim_left", Box::new(TrimStartFn::new()));
runtime.register_function("trim_right", Box::new(TrimEndFn::new()));
runtime.register_function("split", Box::new(SplitFn::new()));
runtime.register_function("replace", Box::new(ReplaceFn::new()));
runtime.register_function("pad_left", Box::new(PadLeftFn::new()));
runtime.register_function("pad_right", Box::new(PadRightFn::new()));
runtime.register_function("substr", Box::new(SubstrFn::new()));
runtime.register_function("capitalize", Box::new(CapitalizeFn::new()));
runtime.register_function("title", Box::new(TitleFn::new()));
runtime.register_function("repeat", Box::new(RepeatFn::new()));
runtime.register_function("find_first", Box::new(IndexOfFn::new()));
runtime.register_function("find_last", Box::new(LastIndexOfFn::new()));
runtime.register_function("slice", Box::new(SliceFn::new()));
runtime.register_function("concat", Box::new(ConcatFn::new()));
runtime.register_function("upper_case", Box::new(UpperCaseFn::new()));
runtime.register_function("lower_case", Box::new(LowerCaseFn::new()));
runtime.register_function("title_case", Box::new(TitleCaseFn::new()));
runtime.register_function("camel_case", Box::new(CamelCaseFn::new()));
runtime.register_function("snake_case", Box::new(SnakeCaseFn::new()));
runtime.register_function("kebab_case", Box::new(KebabCaseFn::new()));
runtime.register_function("pascal_case", Box::new(PascalCaseFn::new()));
runtime.register_function("shouty_snake_case", Box::new(ShoutySnakeCaseFn::new()));
runtime.register_function("shouty_kebab_case", Box::new(ShoutyKebabCaseFn::new()));
runtime.register_function("train_case", Box::new(TrainCaseFn::new()));
runtime.register_function("truncate", Box::new(TruncateFn::new()));
runtime.register_function("wrap", Box::new(WrapFn::new()));
runtime.register_function("format", Box::new(FormatFn::new()));
runtime.register_function("sprintf", Box::new(SprintfFn::new()));
runtime.register_function("ltrimstr", Box::new(LtrimstrFn::new()));
runtime.register_function("rtrimstr", Box::new(RtrimstrFn::new()));
runtime.register_function("indices", Box::new(IndicesFn::new()));
runtime.register_function("inside", Box::new(InsideFn::new()));
runtime.register_function("humanize", Box::new(HumanizeFn::new()));
runtime.register_function("deburr", Box::new(DeburrrFn::new()));
runtime.register_function("words", Box::new(WordsFn::new()));
runtime.register_function("escape", Box::new(EscapeFn::new()));
runtime.register_function("unescape", Box::new(UnescapeFn::new()));
runtime.register_function("escape_regex", Box::new(EscapeRegexFn::new()));
runtime.register_function("start_case", Box::new(StartCaseFn::new()));
runtime.register_function("mask", Box::new(MaskFn::new()));
#[cfg(feature = "regex")]
runtime.register_function("redact", Box::new(RedactFn::new()));
runtime.register_function(
"normalize_whitespace",
Box::new(NormalizeWhitespaceFn::new()),
);
runtime.register_function("is_blank", Box::new(IsBlankFn::new()));
runtime.register_function("abbreviate", Box::new(AbbreviateFn::new()));
runtime.register_function("center", Box::new(CenterFn::new()));
runtime.register_function("reverse_string", Box::new(ReverseStringFn::new()));
runtime.register_function("explode", Box::new(ExplodeFn::new()));
runtime.register_function("implode", Box::new(ImplodeFn::new()));
runtime.register_function("shell_escape", Box::new(ShellEscapeFn::new()));
}
pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
register_if_enabled!(runtime, enabled, "lower", Box::new(LowerFn::new()));
register_if_enabled!(runtime, enabled, "upper", Box::new(UpperFn::new()));
register_if_enabled!(runtime, enabled, "trim", Box::new(TrimFn::new()));
register_if_enabled!(runtime, enabled, "trim_left", Box::new(TrimStartFn::new()));
register_if_enabled!(runtime, enabled, "trim_right", Box::new(TrimEndFn::new()));
register_if_enabled!(runtime, enabled, "split", Box::new(SplitFn::new()));
register_if_enabled!(runtime, enabled, "replace", Box::new(ReplaceFn::new()));
register_if_enabled!(runtime, enabled, "pad_left", Box::new(PadLeftFn::new()));
register_if_enabled!(runtime, enabled, "pad_right", Box::new(PadRightFn::new()));
register_if_enabled!(runtime, enabled, "substr", Box::new(SubstrFn::new()));
register_if_enabled!(
runtime,
enabled,
"capitalize",
Box::new(CapitalizeFn::new())
);
register_if_enabled!(runtime, enabled, "title", Box::new(TitleFn::new()));
register_if_enabled!(runtime, enabled, "repeat", Box::new(RepeatFn::new()));
register_if_enabled!(runtime, enabled, "find_first", Box::new(IndexOfFn::new()));
register_if_enabled!(
runtime,
enabled,
"find_last",
Box::new(LastIndexOfFn::new())
);
register_if_enabled!(runtime, enabled, "slice", Box::new(SliceFn::new()));
register_if_enabled!(runtime, enabled, "concat", Box::new(ConcatFn::new()));
register_if_enabled!(runtime, enabled, "upper_case", Box::new(UpperCaseFn::new()));
register_if_enabled!(runtime, enabled, "lower_case", Box::new(LowerCaseFn::new()));
register_if_enabled!(runtime, enabled, "title_case", Box::new(TitleCaseFn::new()));
register_if_enabled!(runtime, enabled, "camel_case", Box::new(CamelCaseFn::new()));
register_if_enabled!(runtime, enabled, "snake_case", Box::new(SnakeCaseFn::new()));
register_if_enabled!(runtime, enabled, "kebab_case", Box::new(KebabCaseFn::new()));
register_if_enabled!(
runtime,
enabled,
"pascal_case",
Box::new(PascalCaseFn::new())
);
register_if_enabled!(
runtime,
enabled,
"shouty_snake_case",
Box::new(ShoutySnakeCaseFn::new())
);
register_if_enabled!(
runtime,
enabled,
"shouty_kebab_case",
Box::new(ShoutyKebabCaseFn::new())
);
register_if_enabled!(runtime, enabled, "train_case", Box::new(TrainCaseFn::new()));
register_if_enabled!(runtime, enabled, "truncate", Box::new(TruncateFn::new()));
register_if_enabled!(runtime, enabled, "wrap", Box::new(WrapFn::new()));
register_if_enabled!(runtime, enabled, "format", Box::new(FormatFn::new()));
register_if_enabled!(runtime, enabled, "sprintf", Box::new(SprintfFn::new()));
register_if_enabled!(runtime, enabled, "ltrimstr", Box::new(LtrimstrFn::new()));
register_if_enabled!(runtime, enabled, "rtrimstr", Box::new(RtrimstrFn::new()));
register_if_enabled!(runtime, enabled, "indices", Box::new(IndicesFn::new()));
register_if_enabled!(runtime, enabled, "inside", Box::new(InsideFn::new()));
register_if_enabled!(runtime, enabled, "humanize", Box::new(HumanizeFn::new()));
register_if_enabled!(runtime, enabled, "deburr", Box::new(DeburrrFn::new()));
register_if_enabled!(runtime, enabled, "words", Box::new(WordsFn::new()));
register_if_enabled!(runtime, enabled, "escape", Box::new(EscapeFn::new()));
register_if_enabled!(runtime, enabled, "unescape", Box::new(UnescapeFn::new()));
register_if_enabled!(
runtime,
enabled,
"escape_regex",
Box::new(EscapeRegexFn::new())
);
register_if_enabled!(runtime, enabled, "start_case", Box::new(StartCaseFn::new()));
register_if_enabled!(runtime, enabled, "mask", Box::new(MaskFn::new()));
#[cfg(feature = "regex")]
register_if_enabled!(runtime, enabled, "redact", Box::new(RedactFn::new()));
register_if_enabled!(
runtime,
enabled,
"normalize_whitespace",
Box::new(NormalizeWhitespaceFn::new())
);
register_if_enabled!(runtime, enabled, "is_blank", Box::new(IsBlankFn::new()));
register_if_enabled!(
runtime,
enabled,
"abbreviate",
Box::new(AbbreviateFn::new())
);
register_if_enabled!(runtime, enabled, "center", Box::new(CenterFn::new()));
register_if_enabled!(
runtime,
enabled,
"reverse_string",
Box::new(ReverseStringFn::new())
);
register_if_enabled!(runtime, enabled, "explode", Box::new(ExplodeFn::new()));
register_if_enabled!(runtime, enabled, "implode", Box::new(ImplodeFn::new()));
register_if_enabled!(
runtime,
enabled,
"shell_escape",
Box::new(ShellEscapeFn::new())
);
}
define_function!(LowerFn, vec![ArgumentType::String], None);
impl Function for LowerFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_lowercase())))
}
}
define_function!(UpperFn, vec![ArgumentType::String], None);
impl Function for UpperFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_uppercase())))
}
}
define_function!(TrimFn, vec![ArgumentType::String], None);
impl Function for TrimFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.trim().to_string())))
}
}
define_function!(TrimStartFn, vec![ArgumentType::String], None);
impl Function for TrimStartFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.trim_start().to_string())))
}
}
define_function!(TrimEndFn, vec![ArgumentType::String], None);
impl Function for TrimEndFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.trim_end().to_string())))
}
}
define_function!(
SplitFn,
vec![ArgumentType::String, ArgumentType::String],
None
);
impl Function for SplitFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let delimiter = args[1].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string delimiter".to_owned()),
)
})?;
let parts: Vec<Rcvar> = s
.split(delimiter)
.map(|part| Rc::new(Variable::String(part.to_string())) as Rcvar)
.collect();
Ok(Rc::new(Variable::Array(parts)))
}
}
define_function!(
ReplaceFn,
vec![
ArgumentType::String,
ArgumentType::String,
ArgumentType::String
],
None
);
impl Function for ReplaceFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let old = args[1].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected old string argument".to_owned()),
)
})?;
let new = args[2].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected new string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.replace(old, new))))
}
}
define_function!(
PadLeftFn,
vec![
ArgumentType::String,
ArgumentType::Number,
ArgumentType::String
],
None
);
impl Function for PadLeftFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let width = args[1].as_number().map(|n| n as usize).ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected positive number for width".to_owned()),
)
})?;
let pad_char = args[2].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string for pad character".to_owned()),
)
})?;
let pad = pad_char.chars().next().unwrap_or(' ');
let result = if s.len() >= width {
s.to_string()
} else {
format!("{}{}", pad.to_string().repeat(width - s.len()), s)
};
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(
PadRightFn,
vec![
ArgumentType::String,
ArgumentType::Number,
ArgumentType::String
],
None
);
impl Function for PadRightFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let width = args[1].as_number().map(|n| n as usize).ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected positive number for width".to_owned()),
)
})?;
let pad_char = args[2].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string for pad character".to_owned()),
)
})?;
let pad = pad_char.chars().next().unwrap_or(' ');
let result = if s.len() >= width {
s.to_string()
} else {
format!("{}{}", s, pad.to_string().repeat(width - s.len()))
};
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(
SubstrFn,
vec![ArgumentType::String, ArgumentType::Number],
Some(ArgumentType::Number)
);
impl Function for SubstrFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let start = args[1].as_number().map(|n| n as i64).ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number for start".to_owned()),
)
})?;
let start_idx = if start < 0 {
(s.len() as i64 + start).max(0) as usize
} else {
start as usize
};
let result = if args.len() > 2 {
let length = args[2].as_number().map(|n| n as usize).ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected positive number for length".to_owned()),
)
})?;
s.chars().skip(start_idx).take(length).collect()
} else {
s.chars().skip(start_idx).collect()
};
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(CapitalizeFn, vec![ArgumentType::String], None);
impl Function for CapitalizeFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let result = if s.is_empty() {
String::new()
} else {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().to_string() + chars.as_str(),
}
};
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(TitleFn, vec![ArgumentType::String], None);
impl Function for TitleFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let result = s
.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => {
first.to_uppercase().to_string() + &chars.as_str().to_lowercase()
}
}
})
.collect::<Vec<_>>()
.join(" ");
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(
RepeatFn,
vec![ArgumentType::String, ArgumentType::Number],
None
);
impl Function for RepeatFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let count = args[1].as_number().map(|n| n as usize).ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected positive number for count".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.repeat(count))))
}
}
define_function!(
IndexOfFn,
vec![ArgumentType::String, ArgumentType::String],
Some(ArgumentType::Number)
);
fn normalize_index(idx: i64, len: usize) -> usize {
if idx < 0 {
(len as i64 + idx).max(0) as usize
} else {
(idx as usize).min(len)
}
}
impl Function for IndexOfFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let search = args[1].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected search string".to_owned()),
)
})?;
let len = s.len();
let start = if args.len() > 2 {
let start_val = args[2].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number for start".to_owned()),
)
})? as i64;
normalize_index(start_val, len)
} else {
0
};
let end = if args.len() > 3 {
let end_val = args[3].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number for end".to_owned()),
)
})? as i64;
normalize_index(end_val, len)
} else {
len
};
if start >= end || start >= len {
return Ok(Rc::new(Variable::Null));
}
let slice = &s[start..end.min(len)];
match slice.find(search) {
Some(idx) => Ok(Rc::new(Variable::Number(serde_json::Number::from(
(start + idx) as i64,
)))),
None => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(
LastIndexOfFn,
vec![ArgumentType::String, ArgumentType::String],
Some(ArgumentType::Number)
);
impl Function for LastIndexOfFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let search = args[1].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected search string".to_owned()),
)
})?;
let len = s.len();
let start = if args.len() > 2 {
let start_val = args[2].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number for start".to_owned()),
)
})? as i64;
normalize_index(start_val, len)
} else {
0
};
let end = if args.len() > 3 {
let end_val = args[3].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number for end".to_owned()),
)
})? as i64;
normalize_index(end_val, len)
} else {
len
};
if start >= end || start >= len {
return Ok(Rc::new(Variable::Null));
}
let slice = &s[start..end.min(len)];
match slice.rfind(search) {
Some(idx) => Ok(Rc::new(Variable::Number(serde_json::Number::from(
(start + idx) as i64,
)))),
None => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(
SliceFn,
vec![ArgumentType::String, ArgumentType::Number],
Some(ArgumentType::Number)
);
impl Function for SliceFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let len = s.len() as i64;
let start = args[1].as_number().map(|n| n as i64).ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number for start".to_owned()),
)
})?;
let start_idx = if start < 0 {
(len + start).max(0) as usize
} else {
start.min(len) as usize
};
let end_idx = if args.len() > 2 {
let end = args[2].as_number().map(|n| n as i64).ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number for end".to_owned()),
)
})?;
if end < 0 {
(len + end).max(0) as usize
} else {
end.min(len) as usize
}
} else {
len as usize
};
let result: String = s
.chars()
.skip(start_idx)
.take(end_idx.saturating_sub(start_idx))
.collect();
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(
ConcatFn,
vec![ArgumentType::Array],
Some(ArgumentType::String)
);
impl Function for ConcatFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let arr = args[0].as_array().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected array argument".to_owned()),
)
})?;
let separator = if args.len() > 1 {
args[1]
.as_string()
.map(|s| s.to_string())
.unwrap_or_default()
} else {
String::new()
};
let strings: Vec<String> = arr
.iter()
.filter_map(|v| v.as_string().map(|s| s.to_string()))
.collect();
Ok(Rc::new(Variable::String(strings.join(&separator))))
}
}
define_function!(UpperCaseFn, vec![ArgumentType::String], None);
impl Function for UpperCaseFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_uppercase())))
}
}
define_function!(LowerCaseFn, vec![ArgumentType::String], None);
impl Function for LowerCaseFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_lowercase())))
}
}
define_function!(TitleCaseFn, vec![ArgumentType::String], None);
impl Function for TitleCaseFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_title_case())))
}
}
define_function!(CamelCaseFn, vec![ArgumentType::String], None);
impl Function for CamelCaseFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_lower_camel_case())))
}
}
define_function!(SnakeCaseFn, vec![ArgumentType::String], None);
impl Function for SnakeCaseFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_snake_case())))
}
}
define_function!(KebabCaseFn, vec![ArgumentType::String], None);
impl Function for KebabCaseFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_kebab_case())))
}
}
define_function!(PascalCaseFn, vec![ArgumentType::String], None);
impl Function for PascalCaseFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_upper_camel_case())))
}
}
define_function!(ShoutySnakeCaseFn, vec![ArgumentType::String], None);
impl Function for ShoutySnakeCaseFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_shouty_snake_case())))
}
}
define_function!(ShoutyKebabCaseFn, vec![ArgumentType::String], None);
impl Function for ShoutyKebabCaseFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_shouty_kebab_case())))
}
}
define_function!(TrainCaseFn, vec![ArgumentType::String], None);
impl Function for TrainCaseFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::String(s.to_train_case())))
}
}
define_function!(
TruncateFn,
vec![ArgumentType::String, ArgumentType::Number],
Some(ArgumentType::String)
);
impl Function for TruncateFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let max_len = args[1].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number for length".to_owned()),
)
})? as usize;
let suffix = args
.get(2)
.and_then(|v| v.as_string())
.map(|s| s.to_string())
.unwrap_or_else(|| "...".to_string());
if s.len() <= max_len {
Ok(Rc::new(Variable::String(s.to_string())))
} else {
let truncate_at = max_len.saturating_sub(suffix.len());
let truncated: String = s.chars().take(truncate_at).collect();
Ok(Rc::new(Variable::String(format!(
"{}{}",
truncated, suffix
))))
}
}
}
define_function!(
WrapFn,
vec![ArgumentType::String, ArgumentType::Number],
None
);
impl Function for WrapFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let width = args[1].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number for width".to_owned()),
)
})? as usize;
if width == 0 {
return Ok(Rc::new(Variable::String(s.to_string())));
}
let mut lines: Vec<String> = Vec::new();
for paragraph in s.split('\n') {
let mut current_line = String::new();
for word in paragraph.split_whitespace() {
if current_line.is_empty() {
current_line = word.to_string();
} else if current_line.len() + 1 + word.len() <= width {
current_line.push(' ');
current_line.push_str(word);
} else {
lines.push(current_line);
current_line = word.to_string();
}
}
lines.push(current_line);
}
if !s.ends_with('\n') && lines.last().is_some_and(|l| l.is_empty()) {
lines.pop();
}
if lines.is_empty() && !s.is_empty() {
return Ok(Rc::new(Variable::String(s.to_string())));
}
Ok(Rc::new(Variable::String(lines.join("\n"))))
}
}
define_function!(
FormatFn,
vec![ArgumentType::String],
Some(ArgumentType::Any)
);
fn var_to_format_string(v: &Variable) -> String {
match v {
Variable::String(s) => s.clone(),
Variable::Number(n) => n.to_string(),
Variable::Bool(b) => b.to_string(),
Variable::Null => "null".to_string(),
_ => serde_json::to_string(v).unwrap_or_else(|_| "null".to_string()),
}
}
impl Function for FormatFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let template = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected template string".to_owned()),
)
})?;
let mut result = template.to_string();
if args.len() == 2 {
if let Some(arr) = args[1].as_array() {
for (i, item) in arr.iter().enumerate() {
let placeholder = format!("{{{}}}", i);
let value = var_to_format_string(item);
result = result.replace(&placeholder, &value);
}
return Ok(Rc::new(Variable::String(result)));
} else if let Some(obj) = args[1].as_object() {
for (key, val) in obj.iter() {
let placeholder = format!("{{{}}}", key);
let value = var_to_format_string(val);
result = result.replace(&placeholder, &value);
}
return Ok(Rc::new(Variable::String(result)));
}
}
for (i, arg) in args.iter().skip(1).enumerate() {
let placeholder = format!("{{{}}}", i);
let value = var_to_format_string(arg);
result = result.replace(&placeholder, &value);
}
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(
SprintfFn,
vec![ArgumentType::String],
Some(ArgumentType::Any)
);
impl Function for SprintfFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let format_str = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected format string".to_owned()),
)
})?;
let format_args: Vec<&Variable> = if args.len() == 2 {
if let Some(arr) = args[1].as_array() {
arr.iter().map(|v| v.as_ref()).collect()
} else {
args.iter().skip(1).map(|v| v.as_ref()).collect()
}
} else {
args.iter().skip(1).map(|v| v.as_ref()).collect()
};
let mut result = String::new();
let mut arg_index = 0;
let mut chars = format_str.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
if let Some(&next) = chars.peek() {
if next == '%' {
result.push('%');
chars.next();
continue;
}
let mut width = String::new();
let mut precision = String::new();
let mut in_precision = false;
while let Some(&ch) = chars.peek() {
if ch == '.' {
in_precision = true;
chars.next();
} else if ch.is_ascii_digit() || ch == '-' || ch == '+' {
if in_precision {
precision.push(ch);
} else {
width.push(ch);
}
chars.next();
} else {
break;
}
}
if let Some(fmt_type) = chars.next() {
if arg_index < format_args.len() {
let arg = format_args[arg_index];
arg_index += 1;
let formatted = match fmt_type {
's' => var_to_format_string(arg),
'd' | 'i' => {
if let Some(n) = arg.as_number() {
format!("{}", n as i64)
} else {
"0".to_string()
}
}
'f' => {
if let Some(n) = arg.as_number() {
let prec: usize = precision.parse().unwrap_or(6);
format!("{:.prec$}", n, prec = prec)
} else {
"0.0".to_string()
}
}
'e' => {
if let Some(n) = arg.as_number() {
let prec: usize = precision.parse().unwrap_or(6);
format!("{:.prec$e}", n, prec = prec)
} else {
"0e0".to_string()
}
}
'x' => {
if let Some(n) = arg.as_number() {
format!("{:x}", n as i64)
} else {
"0".to_string()
}
}
'X' => {
if let Some(n) = arg.as_number() {
format!("{:X}", n as i64)
} else {
"0".to_string()
}
}
'o' => {
if let Some(n) = arg.as_number() {
format!("{:o}", n as i64)
} else {
"0".to_string()
}
}
'b' => {
if let Some(n) = arg.as_number() {
format!("{:b}", n as i64)
} else {
"0".to_string()
}
}
'c' => {
if let Some(n) = arg.as_number() {
char::from_u32(n as u32)
.map(|c| c.to_string())
.unwrap_or_default()
} else if let Some(s) = arg.as_string() {
s.chars().next().map(|c| c.to_string()).unwrap_or_default()
} else {
String::new()
}
}
_ => {
format!("%{}{}", width, fmt_type)
}
};
if !width.is_empty() {
let w: i32 = width.parse().unwrap_or(0);
if w < 0 {
result.push_str(&format!(
"{:<width$}",
formatted,
width = w.unsigned_abs() as usize
));
} else {
result.push_str(&format!(
"{:>width$}",
formatted,
width = w as usize
));
}
} else {
result.push_str(&formatted);
}
} else {
result.push('%');
result.push_str(&width);
if !precision.is_empty() {
result.push('.');
result.push_str(&precision);
}
result.push(fmt_type);
}
}
} else {
result.push('%');
}
} else {
result.push(c);
}
}
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(
LtrimstrFn,
vec![ArgumentType::String, ArgumentType::String],
None
);
impl Function for LtrimstrFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let prefix = args[1].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected prefix string".to_owned()),
)
})?;
let result = s.strip_prefix(prefix).unwrap_or(s).to_string();
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(
RtrimstrFn,
vec![ArgumentType::String, ArgumentType::String],
None
);
impl Function for RtrimstrFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let suffix = args[1].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected suffix string".to_owned()),
)
})?;
let result = s.strip_suffix(suffix).unwrap_or(s).to_string();
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(
IndicesFn,
vec![ArgumentType::String, ArgumentType::String],
None
);
impl Function for IndicesFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let search = args[1].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected search string".to_owned()),
)
})?;
let mut indices: Vec<Rcvar> = Vec::new();
if !search.is_empty() {
let mut start = 0;
while let Some(pos) = s[start..].find(search) {
let actual_pos = start + pos;
indices.push(Rc::new(Variable::Number(serde_json::Number::from(
actual_pos as i64,
))));
start = actual_pos + 1; }
}
Ok(Rc::new(Variable::Array(indices)))
}
}
define_function!(
InsideFn,
vec![ArgumentType::String, ArgumentType::String],
None
);
impl Function for InsideFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let search = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected search string".to_owned()),
)
})?;
let s = args[1].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
Ok(Rc::new(Variable::Bool(s.contains(search))))
}
}
define_function!(HumanizeFn, vec![ArgumentType::String], None);
impl Function for HumanizeFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let mut result = String::new();
let mut prev_was_lower = false;
let mut word_start = true;
for c in s.chars() {
if c == '_' || c == '-' {
if !result.is_empty() && !result.ends_with(' ') {
result.push(' ');
}
word_start = true;
prev_was_lower = false;
} else if c.is_uppercase() && prev_was_lower {
result.push(' ');
if word_start {
result.push(c); } else {
result.push(c.to_lowercase().next().unwrap_or(c));
}
word_start = false;
prev_was_lower = false;
} else {
if word_start && result.is_empty() {
result.push(c.to_uppercase().next().unwrap_or(c));
} else {
result.push(c.to_lowercase().next().unwrap_or(c));
}
prev_was_lower = c.is_lowercase();
word_start = false;
}
}
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(DeburrrFn, vec![ArgumentType::String], None);
impl Function for DeburrrFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let result: String = s
.chars()
.map(|c| match c {
'À' | 'Á' | 'Â' | 'Ã' | 'Ä' | 'Å' => 'A',
'Æ' => 'A', 'Ç' => 'C',
'È' | 'É' | 'Ê' | 'Ë' => 'E',
'Ì' | 'Í' | 'Î' | 'Ï' => 'I',
'Ð' => 'D',
'Ñ' => 'N',
'Ò' | 'Ó' | 'Ô' | 'Õ' | 'Ö' | 'Ø' => 'O',
'Ù' | 'Ú' | 'Û' | 'Ü' => 'U',
'Ý' => 'Y',
'Þ' => 'T', 'ß' => 's', 'à' | 'á' | 'â' | 'ã' | 'ä' | 'å' => 'a',
'æ' => 'a', 'ç' => 'c',
'è' | 'é' | 'ê' | 'ë' => 'e',
'ì' | 'í' | 'î' | 'ï' => 'i',
'ð' => 'd',
'ñ' => 'n',
'ò' | 'ó' | 'ô' | 'õ' | 'ö' | 'ø' => 'o',
'ù' | 'ú' | 'û' | 'ü' => 'u',
'ý' | 'ÿ' => 'y',
'þ' => 't', _ => c,
})
.collect();
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(WordsFn, vec![ArgumentType::String], None);
impl Function for WordsFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let mut words = Vec::new();
let mut current_word = String::new();
let mut prev_was_lower = false;
for c in s.chars() {
if c.is_whitespace() || c == '_' || c == '-' {
if !current_word.is_empty() {
words.push(Rc::new(Variable::String(current_word.clone())) as Rcvar);
current_word.clear();
}
prev_was_lower = false;
} else if c.is_uppercase() && prev_was_lower {
if !current_word.is_empty() {
words.push(Rc::new(Variable::String(current_word.clone())) as Rcvar);
current_word.clear();
}
current_word.push(c);
prev_was_lower = false;
} else {
current_word.push(c);
prev_was_lower = c.is_lowercase();
}
}
if !current_word.is_empty() {
words.push(Rc::new(Variable::String(current_word)) as Rcvar);
}
Ok(Rc::new(Variable::Array(words)))
}
}
define_function!(EscapeFn, vec![ArgumentType::String], None);
impl Function for EscapeFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let result = s
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'");
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(UnescapeFn, vec![ArgumentType::String], None);
impl Function for UnescapeFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let result = s
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'");
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(EscapeRegexFn, vec![ArgumentType::String], None);
impl Function for EscapeRegexFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let mut result = String::with_capacity(s.len() * 2);
for c in s.chars() {
match c {
'\\' | '^' | '$' | '.' | '|' | '?' | '*' | '+' | '(' | ')' | '[' | ']' | '{'
| '}' => {
result.push('\\');
result.push(c);
}
_ => result.push(c),
}
}
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(StartCaseFn, vec![ArgumentType::String], None);
impl Function for StartCaseFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let mut result = String::new();
let mut prev_was_lower = false;
let mut word_start = true;
for c in s.chars() {
if c.is_whitespace() || c == '_' || c == '-' {
if !result.is_empty() && !result.ends_with(' ') {
result.push(' ');
}
word_start = true;
prev_was_lower = false;
} else if c.is_uppercase() && prev_was_lower {
result.push(' ');
result.push(c); word_start = false;
prev_was_lower = false;
} else {
if word_start {
result.push(c.to_uppercase().next().unwrap_or(c));
} else {
result.push(c.to_lowercase().next().unwrap_or(c));
}
prev_was_lower = c.is_lowercase();
word_start = false;
}
}
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(MaskFn, vec![ArgumentType::String], Some(ArgumentType::Any));
impl Function for MaskFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let visible = if args.len() > 1 && !args[1].is_null() {
args[1].as_number().unwrap_or(0.0) as usize
} else {
0
};
let mask_char = if args.len() > 2 && !args[2].is_null() {
args[2]
.as_string()
.and_then(|s| s.chars().next())
.unwrap_or('*')
} else {
'*'
};
let char_count = s.chars().count();
if visible >= char_count {
return Ok(Rc::new(Variable::String(s.to_string())));
}
let mask_count = char_count - visible;
let masked: String = std::iter::repeat_n(mask_char, mask_count)
.chain(s.chars().skip(mask_count))
.collect();
Ok(Rc::new(Variable::String(masked)))
}
}
#[cfg(feature = "regex")]
define_function!(
RedactFn,
vec![ArgumentType::String, ArgumentType::String],
Some(ArgumentType::Any)
);
#[cfg(feature = "regex")]
impl Function for RedactFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let pattern = args[1].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string pattern".to_owned()),
)
})?;
let replacement = if args.len() > 2 && !args[2].is_null() {
args[2]
.as_string()
.map(|s| s.to_string())
.unwrap_or_else(|| "[REDACTED]".to_string())
} else {
"[REDACTED]".to_string()
};
let re = regex::Regex::new(pattern).map_err(|e| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse(format!("Invalid regex pattern: {}", e)),
)
})?;
let result = re.replace_all(s, replacement.as_str());
Ok(Rc::new(Variable::String(result.into_owned())))
}
}
define_function!(NormalizeWhitespaceFn, vec![ArgumentType::String], None);
impl Function for NormalizeWhitespaceFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let result: String = s.split_whitespace().collect::<Vec<_>>().join(" ");
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(IsBlankFn, vec![ArgumentType::String], None);
impl Function for IsBlankFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let is_blank = s.trim().is_empty();
Ok(Rc::new(Variable::Bool(is_blank)))
}
}
define_function!(
AbbreviateFn,
vec![ArgumentType::String, ArgumentType::Number],
Some(ArgumentType::Any)
);
impl Function for AbbreviateFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let max_length = args[1].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number for max_length".to_owned()),
)
})? as usize;
let suffix = if args.len() > 2 && !args[2].is_null() {
args[2]
.as_string()
.map(|s| s.to_string())
.unwrap_or_else(|| "...".to_string())
} else {
"...".to_string()
};
let char_count = s.chars().count();
let suffix_len = suffix.chars().count();
if char_count <= max_length {
return Ok(Rc::new(Variable::String(s.to_string())));
}
if max_length <= suffix_len {
let result: String = s.chars().take(max_length).collect();
return Ok(Rc::new(Variable::String(result)));
}
let truncate_at = max_length - suffix_len;
let mut result: String = s.chars().take(truncate_at).collect();
result.push_str(&suffix);
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(
CenterFn,
vec![ArgumentType::String, ArgumentType::Number],
Some(ArgumentType::Any)
);
impl Function for CenterFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let width = args[1].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number for width".to_owned()),
)
})? as usize;
let pad_char = if args.len() > 2 && !args[2].is_null() {
args[2]
.as_string()
.and_then(|s| s.chars().next())
.unwrap_or(' ')
} else {
' '
};
let char_count = s.chars().count();
if char_count >= width {
return Ok(Rc::new(Variable::String(s.to_string())));
}
let total_padding = width - char_count;
let left_padding = total_padding / 2;
let right_padding = total_padding - left_padding;
let mut result = String::with_capacity(width);
for _ in 0..left_padding {
result.push(pad_char);
}
result.push_str(s);
for _ in 0..right_padding {
result.push(pad_char);
}
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(ReverseStringFn, vec![ArgumentType::String], None);
impl Function for ReverseStringFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let result: String = s.chars().rev().collect();
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(ExplodeFn, vec![ArgumentType::String], None);
impl Function for ExplodeFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let codepoints: Vec<Rcvar> = s
.chars()
.map(|c| Rc::new(Variable::Number(serde_json::Number::from(c as u32))) as Rcvar)
.collect();
Ok(Rc::new(Variable::Array(codepoints)))
}
}
define_function!(ImplodeFn, vec![ArgumentType::Array], None);
impl Function for ImplodeFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let arr = args[0].as_array().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected array argument".to_owned()),
)
})?;
let mut result = String::new();
for item in arr.iter() {
let codepoint = item.as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected array of numbers (codepoints)".to_owned()),
)
})? as u32;
let c = char::from_u32(codepoint).ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse(format!("Invalid Unicode codepoint: {}", codepoint)),
)
})?;
result.push(c);
}
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(ShellEscapeFn, vec![ArgumentType::String], None);
impl Function for ShellEscapeFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string argument".to_owned()),
)
})?;
let escaped = if s.is_empty() {
"''".to_string()
} else if s
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/')
{
s.to_string()
} else if !s.contains('\'') {
format!("'{}'", s)
} else {
let mut result = String::with_capacity(s.len() + 10);
result.push('\'');
for c in s.chars() {
if c == '\'' {
result.push_str("'\\''");
} else {
result.push(c);
}
}
result.push('\'');
result
};
Ok(Rc::new(Variable::String(escaped)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use jmespath::Runtime;
fn setup_runtime() -> Runtime {
let mut runtime = Runtime::new();
runtime.register_builtin_functions();
register(&mut runtime);
runtime
}
#[test]
fn test_lower() {
let runtime = setup_runtime();
let expr = runtime.compile("lower(@)").unwrap();
let data = Variable::String("HELLO".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello");
}
#[test]
fn test_upper() {
let runtime = setup_runtime();
let expr = runtime.compile("upper(@)").unwrap();
let data = Variable::String("hello".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "HELLO");
}
#[test]
fn test_trim() {
let runtime = setup_runtime();
let expr = runtime.compile("trim(@)").unwrap();
let data = Variable::String(" hello ".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello");
}
#[test]
fn test_split() {
let runtime = setup_runtime();
let expr = runtime.compile("split(@, ',')").unwrap();
let data = Variable::String("a,b,c".to_string());
let result = expr.search(&data).unwrap();
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 3);
assert_eq!(arr[0].as_string().unwrap(), "a");
}
#[test]
fn test_camel_case() {
let runtime = setup_runtime();
let expr = runtime.compile("camel_case(@)").unwrap();
let data = Variable::String("hello_world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "helloWorld");
}
#[test]
fn test_snake_case() {
let runtime = setup_runtime();
let expr = runtime.compile("snake_case(@)").unwrap();
let data = Variable::String("helloWorld".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello_world");
}
#[test]
fn test_wrap_basic() {
let runtime = setup_runtime();
let expr = runtime.compile("wrap(@, `5`)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello\nworld");
}
#[test]
fn test_wrap_preserves_newlines() {
let runtime = setup_runtime();
let expr = runtime.compile("wrap(@, `100`)").unwrap();
let data = Variable::String("hello\nworld".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello\nworld");
}
#[test]
fn test_wrap_wide_width() {
let runtime = setup_runtime();
let expr = runtime.compile("wrap(@, `100`)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello world");
}
#[test]
fn test_ltrimstr() {
let runtime = setup_runtime();
let expr = runtime.compile("ltrimstr(@, 'hello ')").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "world");
}
#[test]
fn test_ltrimstr_no_match() {
let runtime = setup_runtime();
let expr = runtime.compile("ltrimstr(@, 'foo')").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello world");
}
#[test]
fn test_rtrimstr() {
let runtime = setup_runtime();
let expr = runtime.compile("rtrimstr(@, ' world')").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello");
}
#[test]
fn test_rtrimstr_no_match() {
let runtime = setup_runtime();
let expr = runtime.compile("rtrimstr(@, 'foo')").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello world");
}
#[test]
fn test_indices() {
let runtime = setup_runtime();
let expr = runtime.compile("indices(@, 'l')").unwrap();
let data = Variable::String("hello".to_string());
let result = expr.search(&data).unwrap();
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].as_number().unwrap() as i64, 2);
assert_eq!(arr[1].as_number().unwrap() as i64, 3);
}
#[test]
fn test_indices_no_match() {
let runtime = setup_runtime();
let expr = runtime.compile("indices(@, 'x')").unwrap();
let data = Variable::String("hello".to_string());
let result = expr.search(&data).unwrap();
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 0);
}
#[test]
fn test_indices_overlapping() {
let runtime = setup_runtime();
let expr = runtime.compile("indices(@, 'aa')").unwrap();
let data = Variable::String("aaa".to_string());
let result = expr.search(&data).unwrap();
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].as_number().unwrap() as i64, 0);
assert_eq!(arr[1].as_number().unwrap() as i64, 1);
}
#[test]
fn test_inside() {
let runtime = setup_runtime();
let expr = runtime.compile("inside('world', @)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_inside_not_found() {
let runtime = setup_runtime();
let expr = runtime.compile("inside('foo', @)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert!(!result.as_boolean().unwrap());
}
#[test]
fn test_format_with_array() {
let runtime = setup_runtime();
let expr = runtime
.compile("format('Hello {0}, you have {1} messages', @)")
.unwrap();
let data: Variable = serde_json::from_str(r#"["Alice", 5]"#).unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(
result.as_string().unwrap(),
"Hello Alice, you have 5 messages"
);
}
#[test]
fn test_format_with_object() {
let runtime = setup_runtime();
let expr = runtime.compile("format('Hello {name}!', @)").unwrap();
let data: Variable = serde_json::from_str(r#"{"name": "World"}"#).unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "Hello World!");
}
#[test]
fn test_sprintf_string() {
let runtime = setup_runtime();
let expr = runtime.compile("sprintf('Hello, %s!', @)").unwrap();
let data = Variable::String("World".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "Hello, World!");
}
#[test]
fn test_sprintf_integer() {
let runtime = setup_runtime();
let expr = runtime.compile("sprintf('%d + %d = %d', @)").unwrap();
let data: Variable = serde_json::from_str("[1, 2, 3]").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "1 + 2 = 3");
}
#[test]
fn test_sprintf_float_precision() {
let runtime = setup_runtime();
let expr = runtime.compile("sprintf('Pi is %.2f', @)").unwrap();
let data: Variable = serde_json::from_str("3.14159").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "Pi is 3.14");
}
#[test]
fn test_sprintf_hex() {
let runtime = setup_runtime();
let expr = runtime.compile("sprintf('Hex: %x', @)").unwrap();
let data: Variable = serde_json::from_str("255").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "Hex: ff");
}
#[test]
fn test_sprintf_width() {
let runtime = setup_runtime();
let expr = runtime.compile("sprintf('%10s', @)").unwrap();
let data = Variable::String("hi".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), " hi");
}
#[test]
fn test_sprintf_escaped_percent() {
let runtime = setup_runtime();
let expr = runtime.compile("sprintf('100%% done', @)").unwrap();
let data = Variable::Null;
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "100% done");
}
#[test]
fn test_humanize_snake_case() {
let runtime = setup_runtime();
let expr = runtime.compile("humanize(@)").unwrap();
let data = Variable::String("hello_world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "Hello world");
}
#[test]
fn test_humanize_camel_case() {
let runtime = setup_runtime();
let expr = runtime.compile("humanize(@)").unwrap();
let data = Variable::String("helloWorld".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "Hello world");
}
#[test]
fn test_humanize_kebab_case() {
let runtime = setup_runtime();
let expr = runtime.compile("humanize(@)").unwrap();
let data = Variable::String("hello-world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "Hello world");
}
#[test]
fn test_deburr() {
let runtime = setup_runtime();
let expr = runtime.compile("deburr(@)").unwrap();
let data = Variable::String("déjà vu".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "deja vu");
}
#[test]
fn test_deburr_accents() {
let runtime = setup_runtime();
let expr = runtime.compile("deburr(@)").unwrap();
let data = Variable::String("àéîõü".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "aeiou");
}
#[test]
fn test_words() {
let runtime = setup_runtime();
let expr = runtime.compile("words(@)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].as_string().unwrap(), "hello");
assert_eq!(arr[1].as_string().unwrap(), "world");
}
#[test]
fn test_words_camel_case() {
let runtime = setup_runtime();
let expr = runtime.compile("words(@)").unwrap();
let data = Variable::String("helloWorld".to_string());
let result = expr.search(&data).unwrap();
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].as_string().unwrap(), "hello");
assert_eq!(arr[1].as_string().unwrap(), "World");
}
#[test]
fn test_escape() {
let runtime = setup_runtime();
let expr = runtime.compile("escape(@)").unwrap();
let data = Variable::String("<div class=\"test\">Hello & World</div>".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(
result.as_string().unwrap(),
"<div class="test">Hello & World</div>"
);
}
#[test]
fn test_unescape() {
let runtime = setup_runtime();
let expr = runtime.compile("unescape(@)").unwrap();
let data = Variable::String("<div>Hello & World</div>".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "<div>Hello & World</div>");
}
#[test]
fn test_escape_unescape_roundtrip() {
let runtime = setup_runtime();
let expr = runtime.compile("unescape(escape(@))").unwrap();
let data = Variable::String("<div>Hello & World</div>".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "<div>Hello & World</div>");
}
#[test]
fn test_escape_regex() {
let runtime = setup_runtime();
let expr = runtime.compile("escape_regex(@)").unwrap();
let data = Variable::String("hello.world?".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello\\.world\\?");
}
#[test]
fn test_escape_regex_special_chars() {
let runtime = setup_runtime();
let expr = runtime.compile("escape_regex(@)").unwrap();
let data = Variable::String("[a-z]+(foo|bar)*".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(
result.as_string().unwrap(),
"\\[a-z\\]\\+\\(foo\\|bar\\)\\*"
);
}
#[test]
fn test_start_case() {
let runtime = setup_runtime();
let expr = runtime.compile("start_case(@)").unwrap();
let data = Variable::String("hello_world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "Hello World");
}
#[test]
fn test_start_case_camel() {
let runtime = setup_runtime();
let expr = runtime.compile("start_case(@)").unwrap();
let data = Variable::String("helloWorld".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "Hello World");
}
#[test]
fn test_find_first_basic() {
let runtime = setup_runtime();
let expr = runtime.compile("find_first(@, 'world')").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap() as i64, 6);
}
#[test]
fn test_find_first_not_found() {
let runtime = setup_runtime();
let expr = runtime.compile("find_first(@, 'xyz')").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert!(result.is_null());
}
#[test]
fn test_find_first_with_start() {
let runtime = setup_runtime();
let expr = runtime.compile("find_first(@, 'o', `5`)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap() as i64, 7);
}
#[test]
fn test_find_first_with_start_and_end() {
let runtime = setup_runtime();
let expr = runtime.compile("find_first(@, 'o', `0`, `5`)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap() as i64, 4);
}
#[test]
fn test_find_first_with_negative_start() {
let runtime = setup_runtime();
let expr = runtime.compile("find_first(@, 'o', `-5`)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap() as i64, 7);
}
#[test]
fn test_find_last_basic() {
let runtime = setup_runtime();
let expr = runtime.compile("find_last(@, 'o')").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap() as i64, 7);
}
#[test]
fn test_find_last_not_found() {
let runtime = setup_runtime();
let expr = runtime.compile("find_last(@, 'xyz')").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert!(result.is_null());
}
#[test]
fn test_find_last_with_start_and_end() {
let runtime = setup_runtime();
let expr = runtime.compile("find_last(@, 'o', `0`, `6`)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap() as i64, 4);
}
#[test]
fn test_find_last_with_negative_end() {
let runtime = setup_runtime();
let expr = runtime.compile("find_last(@, 'l', `0`, `-1`)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap() as i64, 9);
}
#[test]
fn test_mask_all() {
let runtime = setup_runtime();
let expr = runtime.compile("mask(@)").unwrap();
let data = Variable::String("secret".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "******");
}
#[test]
fn test_mask_keep_last_4() {
let runtime = setup_runtime();
let expr = runtime.compile("mask(@, `4`)").unwrap();
let data = Variable::String("4111111111111111".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "************1111");
}
#[test]
fn test_mask_custom_char() {
let runtime = setup_runtime();
let expr = runtime.compile("mask(@, `0`, `\"X\"`)").unwrap();
let data = Variable::String("secret".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "XXXXXX");
}
#[test]
fn test_mask_visible_exceeds_length() {
let runtime = setup_runtime();
let expr = runtime.compile("mask(@, `10`)").unwrap();
let data = Variable::String("short".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "short");
}
#[test]
#[cfg(feature = "regex")]
fn test_redact_email() {
let runtime = setup_runtime();
let expr = runtime
.compile(
r#"redact(@, `"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"`, `"[EMAIL]"`)"#,
)
.unwrap();
let data = Variable::String("Contact us at test@example.com for help".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(
result.as_string().unwrap(),
"Contact us at [EMAIL] for help"
);
}
#[test]
#[cfg(feature = "regex")]
fn test_redact_default_replacement() {
let runtime = setup_runtime();
let expr = runtime.compile(r#"redact(@, `"secret"` )"#).unwrap();
let data = Variable::String("The secret password is secret".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(
result.as_string().unwrap(),
"The [REDACTED] password is [REDACTED]"
);
}
#[test]
fn test_normalize_whitespace_basic() {
let runtime = setup_runtime();
let expr = runtime.compile("normalize_whitespace(@)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello world");
}
#[test]
fn test_normalize_whitespace_mixed() {
let runtime = setup_runtime();
let expr = runtime.compile("normalize_whitespace(@)").unwrap();
let data = Variable::String("hello\t\n world\n\nfoo".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello world foo");
}
#[test]
fn test_normalize_whitespace_leading_trailing() {
let runtime = setup_runtime();
let expr = runtime.compile("normalize_whitespace(@)").unwrap();
let data = Variable::String(" hello world ".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello world");
}
#[test]
fn test_is_blank_empty() {
let runtime = setup_runtime();
let expr = runtime.compile("is_blank(@)").unwrap();
let data = Variable::String("".to_string());
let result = expr.search(&data).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_blank_whitespace() {
let runtime = setup_runtime();
let expr = runtime.compile("is_blank(@)").unwrap();
let data = Variable::String(" \t\n ".to_string());
let result = expr.search(&data).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_blank_not_blank() {
let runtime = setup_runtime();
let expr = runtime.compile("is_blank(@)").unwrap();
let data = Variable::String(" a ".to_string());
let result = expr.search(&data).unwrap();
assert!(!result.as_boolean().unwrap());
}
#[test]
fn test_abbreviate_basic() {
let runtime = setup_runtime();
let expr = runtime.compile("abbreviate(@, `10`)").unwrap();
let data = Variable::String("This is a very long string".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "This is...");
}
#[test]
fn test_abbreviate_no_truncation() {
let runtime = setup_runtime();
let expr = runtime.compile("abbreviate(@, `20`)").unwrap();
let data = Variable::String("short".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "short");
}
#[test]
fn test_abbreviate_custom_suffix() {
let runtime = setup_runtime();
let expr = runtime.compile("abbreviate(@, `8`, `\"~\"`)").unwrap();
let data = Variable::String("Hello World".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "Hello W~");
}
#[test]
fn test_center_basic() {
let runtime = setup_runtime();
let expr = runtime.compile("center(@, `10`)").unwrap();
let data = Variable::String("hi".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), " hi ");
}
#[test]
fn test_center_custom_char() {
let runtime = setup_runtime();
let expr = runtime.compile("center(@, `10`, `\"-\"`)").unwrap();
let data = Variable::String("hi".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "----hi----");
}
#[test]
fn test_center_already_wide() {
let runtime = setup_runtime();
let expr = runtime.compile("center(@, `3`)").unwrap();
let data = Variable::String("hello".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello");
}
#[test]
fn test_center_odd_padding() {
let runtime = setup_runtime();
let expr = runtime.compile("center(@, `7`)").unwrap();
let data = Variable::String("hi".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), " hi ");
}
#[test]
fn test_reverse_string_basic() {
let runtime = setup_runtime();
let expr = runtime.compile("reverse_string(@)").unwrap();
let data = Variable::String("hello".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "olleh");
}
#[test]
fn test_reverse_string_empty() {
let runtime = setup_runtime();
let expr = runtime.compile("reverse_string(@)").unwrap();
let data = Variable::String("".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "");
}
#[test]
fn test_reverse_string_palindrome() {
let runtime = setup_runtime();
let expr = runtime.compile("reverse_string(@)").unwrap();
let data = Variable::String("racecar".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "racecar");
}
#[test]
fn test_explode_basic() {
let runtime = setup_runtime();
let expr = runtime.compile("explode(@)").unwrap();
let data = Variable::String("abc".to_string());
let result = expr.search(&data).unwrap();
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 3);
assert_eq!(arr[0].as_number().unwrap() as u32, 97); assert_eq!(arr[1].as_number().unwrap() as u32, 98); assert_eq!(arr[2].as_number().unwrap() as u32, 99); }
#[test]
fn test_explode_empty() {
let runtime = setup_runtime();
let expr = runtime.compile("explode(@)").unwrap();
let data = Variable::String("".to_string());
let result = expr.search(&data).unwrap();
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 0);
}
#[test]
fn test_explode_unicode() {
let runtime = setup_runtime();
let expr = runtime.compile("explode(@)").unwrap();
let data = Variable::String("A☺".to_string());
let result = expr.search(&data).unwrap();
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].as_number().unwrap() as u32, 65); assert_eq!(arr[1].as_number().unwrap() as u32, 9786); }
#[test]
fn test_implode_basic() {
let runtime = setup_runtime();
let expr = runtime.compile("implode(@)").unwrap();
let data: Variable = serde_json::from_str("[97, 98, 99]").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "abc");
}
#[test]
fn test_implode_empty() {
let runtime = setup_runtime();
let expr = runtime.compile("implode(@)").unwrap();
let data: Variable = serde_json::from_str("[]").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "");
}
#[test]
fn test_implode_unicode() {
let runtime = setup_runtime();
let expr = runtime.compile("implode(@)").unwrap();
let data: Variable = serde_json::from_str("[65, 9786]").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "A☺");
}
#[test]
fn test_explode_implode_roundtrip() {
let runtime = setup_runtime();
let expr = runtime.compile("implode(explode(@))").unwrap();
let data = Variable::String("Hello, 世界!".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "Hello, 世界!");
}
#[test]
fn test_shell_escape_simple() {
let runtime = setup_runtime();
let expr = runtime.compile("shell_escape(@)").unwrap();
let data = Variable::String("hello".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "hello");
}
#[test]
fn test_shell_escape_with_spaces() {
let runtime = setup_runtime();
let expr = runtime.compile("shell_escape(@)").unwrap();
let data = Variable::String("hello world".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "'hello world'");
}
#[test]
fn test_shell_escape_with_special_chars() {
let runtime = setup_runtime();
let expr = runtime.compile("shell_escape(@)").unwrap();
let data = Variable::String("$PATH; rm -rf /".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "'$PATH; rm -rf /'");
}
#[test]
fn test_shell_escape_with_single_quote() {
let runtime = setup_runtime();
let expr = runtime.compile("shell_escape(@)").unwrap();
let data = Variable::String("it's".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "'it'\\''s'");
}
#[test]
fn test_shell_escape_empty() {
let runtime = setup_runtime();
let expr = runtime.compile("shell_escape(@)").unwrap();
let data = Variable::String("".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "''");
}
#[test]
fn test_shell_escape_path() {
let runtime = setup_runtime();
let expr = runtime.compile("shell_escape(@)").unwrap();
let data = Variable::String("/usr/local/bin".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "/usr/local/bin");
}
#[test]
fn test_shell_escape_backticks() {
let runtime = setup_runtime();
let expr = runtime.compile("shell_escape(@)").unwrap();
let data = Variable::String("`whoami`".to_string());
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "'`whoami`'");
}
}