diff --git a/src/stdlib/array.rs b/src/stdlib/array.rs new file mode 100644 index 0000000..7e66205 --- /dev/null +++ b/src/stdlib/array.rs @@ -0,0 +1,497 @@ +//! WokeLang Standard Library - Array Module +//! +//! Array manipulation functions. + +use crate::interpreter::Value; +use crate::security::CapabilityRegistry; +use super::{check_arity, check_arity_range, expect_int, StdlibError}; + +/// Get the length of an array +pub fn length(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + match &args[0] { + Value::Array(a) => Ok(Value::Int(a.len() as i64)), + other => Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }), + } +} + +/// Check if array is empty +pub fn is_empty(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + match &args[0] { + Value::Array(a) => Ok(Value::Bool(a.is_empty())), + other => Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }), + } +} + +/// Get first element of array +pub fn first(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + match &args[0] { + Value::Array(a) => match a.first() { + Some(v) => Ok(Value::Okay(Box::new(v.clone()))), + None => Ok(Value::Oops("array is empty".to_string())), + }, + other => Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }), + } +} + +/// Get last element of array +pub fn last(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + match &args[0] { + Value::Array(a) => match a.last() { + Some(v) => Ok(Value::Okay(Box::new(v.clone()))), + None => Ok(Value::Oops("array is empty".to_string())), + }, + other => Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }), + } +} + +/// Push element to array (returns new array) +pub fn push(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + match &args[0] { + Value::Array(a) => { + let mut new_arr = a.clone(); + new_arr.push(args[1].clone()); + Ok(Value::Array(new_arr)) + } + other => Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }), + } +} + +/// Pop last element from array (returns [new_array, popped_element]) +pub fn pop(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + match &args[0] { + Value::Array(a) => { + if a.is_empty() { + Ok(Value::Oops("array is empty".to_string())) + } else { + let mut new_arr = a.clone(); + let popped = new_arr.pop().unwrap(); + Ok(Value::Array(vec![Value::Array(new_arr), popped])) + } + } + other => Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }), + } +} + +/// Concatenate two arrays +pub fn concat(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + + let arr1 = match &args[0] { + Value::Array(a) => a, + other => { + return Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }) + } + }; + + let arr2 = match &args[1] { + Value::Array(a) => a, + other => { + return Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }) + } + }; + + let mut result = arr1.clone(); + result.extend(arr2.clone()); + Ok(Value::Array(result)) +} + +/// Reverse an array +pub fn reverse(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + match &args[0] { + Value::Array(a) => { + let mut new_arr = a.clone(); + new_arr.reverse(); + Ok(Value::Array(new_arr)) + } + other => Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }), + } +} + +/// Get slice of array +pub fn slice(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity_range(args, 2, 3)?; + + let arr = match &args[0] { + Value::Array(a) => a, + other => { + return Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }) + } + }; + + let len = arr.len() as i64; + let start = expect_int(&args[1], "start")?; + + // Handle negative indices + 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 = expect_int(&args[2], "end")?; + if end < 0 { + ((len + end).max(0)) as usize + } else { + end.min(len) as usize + } + } else { + len as usize + }; + + if start_idx >= end_idx { + return Ok(Value::Array(vec![])); + } + + Ok(Value::Array(arr[start_idx..end_idx].to_vec())) +} + +/// Check if array contains a value +pub fn contains(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + + let arr = match &args[0] { + Value::Array(a) => a, + other => { + return Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }) + } + }; + + Ok(Value::Bool(arr.contains(&args[1]))) +} + +/// Find index of first occurrence of value +pub fn index_of(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + + let arr = match &args[0] { + Value::Array(a) => a, + other => { + return Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }) + } + }; + + match arr.iter().position(|x| x == &args[1]) { + Some(idx) => Ok(Value::Int(idx as i64)), + None => Ok(Value::Int(-1)), + } +} + +/// Create array of repeated value +pub fn repeat(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let count = expect_int(&args[1], "count")?; + + if count < 0 { + return Err(StdlibError::RuntimeError("repeat count cannot be negative".to_string())); + } + + if count > 10000 { + return Err(StdlibError::RuntimeError("repeat count too large (max 10000)".to_string())); + } + + let arr: Vec = std::iter::repeat(args[0].clone()).take(count as usize).collect(); + Ok(Value::Array(arr)) +} + +/// Create a range array from start to end (exclusive) +pub fn range(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity_range(args, 1, 3)?; + + let (start, end, step) = if args.len() == 1 { + (0, expect_int(&args[0], "end")?, 1) + } else if args.len() == 2 { + (expect_int(&args[0], "start")?, expect_int(&args[1], "end")?, 1) + } else { + ( + expect_int(&args[0], "start")?, + expect_int(&args[1], "end")?, + expect_int(&args[2], "step")?, + ) + }; + + if step == 0 { + return Err(StdlibError::RuntimeError("step cannot be zero".to_string())); + } + + // Limit array size to prevent memory exhaustion + let estimated_size = if step > 0 { + ((end - start) / step).max(0) as usize + } else { + ((start - end) / (-step)).max(0) as usize + }; + + if estimated_size > 100000 { + return Err(StdlibError::RuntimeError("range too large (max 100000)".to_string())); + } + + let arr: Vec = if step > 0 { + (start..end).step_by(step as usize).map(Value::Int).collect() + } else { + let mut result = Vec::new(); + let mut i = start; + while i > end { + result.push(Value::Int(i)); + i += step; + } + result + }; + + Ok(Value::Array(arr)) +} + +/// Flatten nested arrays one level +pub fn flatten(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + + let arr = match &args[0] { + Value::Array(a) => a, + other => { + return Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }) + } + }; + + let mut result = Vec::new(); + for item in arr { + match item { + Value::Array(inner) => result.extend(inner.clone()), + other => result.push(other.clone()), + } + } + + Ok(Value::Array(result)) +} + +/// Remove duplicates from array (preserves first occurrence) +pub fn unique(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + + let arr = match &args[0] { + Value::Array(a) => a, + other => { + return Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }) + } + }; + + let mut result = Vec::new(); + for item in arr { + if !result.contains(item) { + result.push(item.clone()); + } + } + + Ok(Value::Array(result)) +} + +/// Zip two arrays together +pub fn zip(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + + let arr1 = match &args[0] { + Value::Array(a) => a, + other => { + return Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }) + } + }; + + let arr2 = match &args[1] { + Value::Array(a) => a, + other => { + return Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }) + } + }; + + let result: Vec = arr1 + .iter() + .zip(arr2.iter()) + .map(|(a, b)| Value::Array(vec![a.clone(), b.clone()])) + .collect(); + + Ok(Value::Array(result)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_caps() -> CapabilityRegistry { + CapabilityRegistry::permissive() + } + + #[test] + fn test_length() { + let mut caps = test_caps(); + let arr = Value::Array(vec![Value::Int(1), Value::Int(2), Value::Int(3)]); + assert_eq!(length(&[arr], &mut caps).unwrap(), Value::Int(3)); + } + + #[test] + fn test_first_last() { + let mut caps = test_caps(); + let arr = Value::Array(vec![Value::Int(1), Value::Int(2), Value::Int(3)]); + + if let Value::Okay(v) = first(&[arr.clone()], &mut caps).unwrap() { + assert_eq!(*v, Value::Int(1)); + } + + if let Value::Okay(v) = last(&[arr], &mut caps).unwrap() { + assert_eq!(*v, Value::Int(3)); + } + } + + #[test] + fn test_push_pop() { + let mut caps = test_caps(); + let arr = Value::Array(vec![Value::Int(1), Value::Int(2)]); + + let pushed = push(&[arr, Value::Int(3)], &mut caps).unwrap(); + assert_eq!( + pushed, + Value::Array(vec![Value::Int(1), Value::Int(2), Value::Int(3)]) + ); + } + + #[test] + fn test_concat() { + let mut caps = test_caps(); + let arr1 = Value::Array(vec![Value::Int(1), Value::Int(2)]); + let arr2 = Value::Array(vec![Value::Int(3), Value::Int(4)]); + + assert_eq!( + concat(&[arr1, arr2], &mut caps).unwrap(), + Value::Array(vec![ + Value::Int(1), + Value::Int(2), + Value::Int(3), + Value::Int(4) + ]) + ); + } + + #[test] + fn test_slice() { + let mut caps = test_caps(); + let arr = Value::Array(vec![ + Value::Int(1), + Value::Int(2), + Value::Int(3), + Value::Int(4), + ]); + + assert_eq!( + slice(&[arr, Value::Int(1), Value::Int(3)], &mut caps).unwrap(), + Value::Array(vec![Value::Int(2), Value::Int(3)]) + ); + } + + #[test] + fn test_range() { + let mut caps = test_caps(); + + assert_eq!( + range(&[Value::Int(5)], &mut caps).unwrap(), + Value::Array(vec![ + Value::Int(0), + Value::Int(1), + Value::Int(2), + Value::Int(3), + Value::Int(4) + ]) + ); + + assert_eq!( + range(&[Value::Int(2), Value::Int(5)], &mut caps).unwrap(), + Value::Array(vec![Value::Int(2), Value::Int(3), Value::Int(4)]) + ); + } + + #[test] + fn test_unique() { + let mut caps = test_caps(); + let arr = Value::Array(vec![ + Value::Int(1), + Value::Int(2), + Value::Int(1), + Value::Int(3), + Value::Int(2), + ]); + + assert_eq!( + unique(&[arr], &mut caps).unwrap(), + Value::Array(vec![Value::Int(1), Value::Int(2), Value::Int(3)]) + ); + } + + #[test] + fn test_zip() { + let mut caps = test_caps(); + let arr1 = Value::Array(vec![Value::Int(1), Value::Int(2)]); + let arr2 = Value::Array(vec![ + Value::String("a".to_string()), + Value::String("b".to_string()), + ]); + + assert_eq!( + zip(&[arr1, arr2], &mut caps).unwrap(), + Value::Array(vec![ + Value::Array(vec![Value::Int(1), Value::String("a".to_string())]), + Value::Array(vec![Value::Int(2), Value::String("b".to_string())]), + ]) + ); + } +} diff --git a/src/stdlib/mod.rs b/src/stdlib/mod.rs index f4ba6b9..0827722 100644 --- a/src/stdlib/mod.rs +++ b/src/stdlib/mod.rs @@ -3,11 +3,13 @@ //! This module provides the standard library for WokeLang, offering //! common functionality with consent-aware operations. +pub mod array; pub mod chan; pub mod io; pub mod json; pub mod math; pub mod net; +pub mod string; pub mod time; use crate::interpreter::Value; @@ -127,6 +129,46 @@ impl StdlibRegistry { self.register("std.chan.recvTimeout", chan::recv_timeout); self.register("std.chan.close", chan::close); self.register("std.chan.isClosed", chan::is_closed); + + // String functions + self.register("std.string.length", string::length); + self.register("std.string.upper", string::upper); + self.register("std.string.lower", string::lower); + self.register("std.string.trim", string::trim); + self.register("std.string.trimStart", string::trim_start); + self.register("std.string.trimEnd", string::trim_end); + self.register("std.string.contains", string::contains); + self.register("std.string.startsWith", string::starts_with); + self.register("std.string.endsWith", string::ends_with); + self.register("std.string.replace", string::replace); + self.register("std.string.split", string::split); + self.register("std.string.join", string::join); + self.register("std.string.substring", string::substring); + self.register("std.string.indexOf", string::index_of); + self.register("std.string.repeat", string::repeat); + self.register("std.string.reverse", string::reverse); + self.register("std.string.padStart", string::pad_start); + self.register("std.string.padEnd", string::pad_end); + self.register("std.string.chars", string::chars); + self.register("std.string.isEmpty", string::is_empty); + + // Array functions + self.register("std.array.length", array::length); + self.register("std.array.isEmpty", array::is_empty); + self.register("std.array.first", array::first); + self.register("std.array.last", array::last); + self.register("std.array.push", array::push); + self.register("std.array.pop", array::pop); + self.register("std.array.concat", array::concat); + self.register("std.array.reverse", array::reverse); + self.register("std.array.slice", array::slice); + self.register("std.array.contains", array::contains); + self.register("std.array.indexOf", array::index_of); + self.register("std.array.repeat", array::repeat); + self.register("std.array.range", array::range); + self.register("std.array.flatten", array::flatten); + self.register("std.array.unique", array::unique); + self.register("std.array.zip", array::zip); } /// Register a function diff --git a/src/stdlib/string.rs b/src/stdlib/string.rs new file mode 100644 index 0000000..12f99ff --- /dev/null +++ b/src/stdlib/string.rs @@ -0,0 +1,367 @@ +//! WokeLang Standard Library - String Module +//! +//! String manipulation functions. + +use crate::interpreter::Value; +use crate::security::CapabilityRegistry; +use super::{check_arity, check_arity_range, expect_int, expect_string, StdlibError}; + +/// Get the length of a string (in characters, not bytes) +pub fn length(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let s = expect_string(&args[0], "string")?; + Ok(Value::Int(s.chars().count() as i64)) +} + +/// Convert string to uppercase +pub fn upper(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let s = expect_string(&args[0], "string")?; + Ok(Value::String(s.to_uppercase())) +} + +/// Convert string to lowercase +pub fn lower(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let s = expect_string(&args[0], "string")?; + Ok(Value::String(s.to_lowercase())) +} + +/// Trim whitespace from both ends +pub fn trim(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let s = expect_string(&args[0], "string")?; + Ok(Value::String(s.trim().to_string())) +} + +/// Trim whitespace from start +pub fn trim_start(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let s = expect_string(&args[0], "string")?; + Ok(Value::String(s.trim_start().to_string())) +} + +/// Trim whitespace from end +pub fn trim_end(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let s = expect_string(&args[0], "string")?; + Ok(Value::String(s.trim_end().to_string())) +} + +/// Check if string contains a substring +pub fn contains(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let s = expect_string(&args[0], "string")?; + let substring = expect_string(&args[1], "substring")?; + Ok(Value::Bool(s.contains(&substring))) +} + +/// Check if string starts with a prefix +pub fn starts_with(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let s = expect_string(&args[0], "string")?; + let prefix = expect_string(&args[1], "prefix")?; + Ok(Value::Bool(s.starts_with(&prefix))) +} + +/// Check if string ends with a suffix +pub fn ends_with(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let s = expect_string(&args[0], "string")?; + let suffix = expect_string(&args[1], "suffix")?; + Ok(Value::Bool(s.ends_with(&suffix))) +} + +/// Replace all occurrences of a pattern +pub fn replace(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 3)?; + let s = expect_string(&args[0], "string")?; + let from = expect_string(&args[1], "from")?; + let to = expect_string(&args[2], "to")?; + Ok(Value::String(s.replace(&from, &to))) +} + +/// Split string by delimiter +pub fn split(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let s = expect_string(&args[0], "string")?; + let delimiter = expect_string(&args[1], "delimiter")?; + let parts: Vec = s.split(&delimiter).map(|p| Value::String(p.to_string())).collect(); + Ok(Value::Array(parts)) +} + +/// Join array of strings with delimiter +pub fn join(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + + let arr = match &args[0] { + Value::Array(a) => a, + other => { + return Err(StdlibError::TypeError { + expected: "Array".to_string(), + got: format!("{:?}", other), + }) + } + }; + + let delimiter = expect_string(&args[1], "delimiter")?; + + let strings: Result, _> = arr + .iter() + .map(|v| match v { + Value::String(s) => Ok(s.clone()), + other => Err(StdlibError::TypeError { + expected: "String".to_string(), + got: format!("{:?}", other), + }), + }) + .collect(); + + Ok(Value::String(strings?.join(&delimiter))) +} + +/// Get substring by start and optional end index +pub fn substring(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity_range(args, 2, 3)?; + let s = expect_string(&args[0], "string")?; + let start = expect_int(&args[1], "start")?; + + let chars: Vec = s.chars().collect(); + let len = chars.len() as i64; + + // Handle negative indices + 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 = expect_int(&args[2], "end")?; + if end < 0 { + ((len + end).max(0)) as usize + } else { + end.min(len) as usize + } + } else { + len as usize + }; + + if start_idx >= end_idx { + return Ok(Value::String(String::new())); + } + + Ok(Value::String(chars[start_idx..end_idx].iter().collect())) +} + +/// Find index of first occurrence of substring +pub fn index_of(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let s = expect_string(&args[0], "string")?; + let needle = expect_string(&args[1], "needle")?; + + match s.find(&needle) { + Some(idx) => { + // Convert byte index to character index + let char_idx = s[..idx].chars().count(); + Ok(Value::Int(char_idx as i64)) + } + None => Ok(Value::Int(-1)), + } +} + +/// Repeat string n times +pub fn repeat(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let s = expect_string(&args[0], "string")?; + let n = expect_int(&args[1], "count")?; + + if n < 0 { + return Err(StdlibError::RuntimeError("repeat count cannot be negative".to_string())); + } + + if n > 10000 { + return Err(StdlibError::RuntimeError("repeat count too large (max 10000)".to_string())); + } + + Ok(Value::String(s.repeat(n as usize))) +} + +/// Reverse a string +pub fn reverse(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let s = expect_string(&args[0], "string")?; + Ok(Value::String(s.chars().rev().collect())) +} + +/// Pad string on the left to reach target length +pub fn pad_start(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity_range(args, 2, 3)?; + let s = expect_string(&args[0], "string")?; + let target_len = expect_int(&args[1], "length")? as usize; + let pad_char = if args.len() > 2 { + let p = expect_string(&args[2], "pad")?; + p.chars().next().unwrap_or(' ') + } else { + ' ' + }; + + let current_len = s.chars().count(); + if current_len >= target_len { + return Ok(Value::String(s)); + } + + let padding: String = std::iter::repeat(pad_char).take(target_len - current_len).collect(); + Ok(Value::String(format!("{}{}", padding, s))) +} + +/// Pad string on the right to reach target length +pub fn pad_end(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity_range(args, 2, 3)?; + let s = expect_string(&args[0], "string")?; + let target_len = expect_int(&args[1], "length")? as usize; + let pad_char = if args.len() > 2 { + let p = expect_string(&args[2], "pad")?; + p.chars().next().unwrap_or(' ') + } else { + ' ' + }; + + let current_len = s.chars().count(); + if current_len >= target_len { + return Ok(Value::String(s)); + } + + let padding: String = std::iter::repeat(pad_char).take(target_len - current_len).collect(); + Ok(Value::String(format!("{}{}", s, padding))) +} + +/// Split string into array of characters +pub fn chars(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let s = expect_string(&args[0], "string")?; + let char_array: Vec = s.chars().map(|c| Value::String(c.to_string())).collect(); + Ok(Value::Array(char_array)) +} + +/// Check if string is empty +pub fn is_empty(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let s = expect_string(&args[0], "string")?; + Ok(Value::Bool(s.is_empty())) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_caps() -> CapabilityRegistry { + CapabilityRegistry::permissive() + } + + #[test] + fn test_length() { + let mut caps = test_caps(); + assert_eq!( + length(&[Value::String("hello".to_string())], &mut caps).unwrap(), + Value::Int(5) + ); + // UTF-8 characters + assert_eq!( + length(&[Value::String("你好".to_string())], &mut caps).unwrap(), + Value::Int(2) + ); + } + + #[test] + fn test_upper_lower() { + let mut caps = test_caps(); + assert_eq!( + upper(&[Value::String("hello".to_string())], &mut caps).unwrap(), + Value::String("HELLO".to_string()) + ); + assert_eq!( + lower(&[Value::String("HELLO".to_string())], &mut caps).unwrap(), + Value::String("hello".to_string()) + ); + } + + #[test] + fn test_trim() { + let mut caps = test_caps(); + assert_eq!( + trim(&[Value::String(" hello ".to_string())], &mut caps).unwrap(), + Value::String("hello".to_string()) + ); + } + + #[test] + fn test_contains() { + let mut caps = test_caps(); + assert_eq!( + contains( + &[Value::String("hello world".to_string()), Value::String("world".to_string())], + &mut caps + ) + .unwrap(), + Value::Bool(true) + ); + } + + #[test] + fn test_split_join() { + let mut caps = test_caps(); + let result = split( + &[Value::String("a,b,c".to_string()), Value::String(",".to_string())], + &mut caps, + ) + .unwrap(); + + assert_eq!( + result, + Value::Array(vec![ + Value::String("a".to_string()), + Value::String("b".to_string()), + Value::String("c".to_string()), + ]) + ); + + let joined = join( + &[result, Value::String("-".to_string())], + &mut caps, + ) + .unwrap(); + + assert_eq!(joined, Value::String("a-b-c".to_string())); + } + + #[test] + fn test_substring() { + let mut caps = test_caps(); + assert_eq!( + substring( + &[Value::String("hello".to_string()), Value::Int(1), Value::Int(4)], + &mut caps + ) + .unwrap(), + Value::String("ell".to_string()) + ); + } + + #[test] + fn test_replace() { + let mut caps = test_caps(); + assert_eq!( + replace( + &[ + Value::String("hello world".to_string()), + Value::String("world".to_string()), + Value::String("rust".to_string()) + ], + &mut caps + ) + .unwrap(), + Value::String("hello rust".to_string()) + ); + } +}