Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/stdlib/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,3 +473,46 @@ pub fn expect_datetime(value: &Value) -> Result<Rc<chrono::NaiveDateTime>, Runti
)),
}
}

/// Helper for unary text operations (Text -> Text)
///
/// Handles argument count checking, type extraction, operation execution,
/// and result wrapping.
///
/// # Arguments
///
/// * `func_name` - Name of the function for error messages
/// * `args` - Arguments passed to the function
/// * `op` - The text operation to perform
pub fn unary_text_op<F>(func_name: &str, args: Vec<Value>, op: F) -> Result<Value, RuntimeError>
where
F: Fn(&str) -> String,
{
check_arg_count(func_name, &args, 1)?;
let text = expect_text(&args[0])?;
Ok(Value::Text(Arc::from(op(&text))))
Comment on lines +491 to +493

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These calls rely on deref coercions from &Arc<str> to &str (op(&text) / op(&a, &b)), which is valid but a bit opaque. For readability and to make the intended types explicit, prefer passing text.as_ref() (and a.as_ref(), b.as_ref()) into the closures.

Copilot uses AI. Check for mistakes.
}

/// Helper for binary text predicates ((Text, Text) -> Bool)
///
/// Handles argument count checking, type extraction, operation execution,
/// and result wrapping.
///
/// # Arguments
///
/// * `func_name` - Name of the function for error messages
/// * `args` - Arguments passed to the function
/// * `op` - The predicate to perform
pub fn binary_text_predicate<F>(
func_name: &str,
args: Vec<Value>,
op: F,
) -> Result<Value, RuntimeError>
where
F: Fn(&str, &str) -> bool,
{
check_arg_count(func_name, &args, 2)?;
let a = expect_text(&args[0])?;
let b = expect_text(&args[1])?;
Ok(Value::Bool(op(&a, &b)))
Comment on lines +514 to +517

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These calls rely on deref coercions from &Arc<str> to &str (op(&text) / op(&a, &b)), which is valid but a bit opaque. For readability and to make the intended types explicit, prefer passing text.as_ref() (and a.as_ref(), b.as_ref()) into the closures.

Copilot uses AI. Check for mistakes.
}
62 changes: 18 additions & 44 deletions src/stdlib/text.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use super::helpers::{check_arg_count, expect_number, expect_text};
use super::helpers::{
binary_text_predicate, check_arg_count, expect_number, expect_text, unary_text_op,
};
use crate::interpreter::environment::Environment;
use crate::interpreter::error::RuntimeError;
use crate::interpreter::value::Value;
Expand Down Expand Up @@ -74,19 +76,11 @@ fn parse_key_value_pairs(input: &str, delimiter: char) -> std::collections::Hash
// which handles both text and lists

pub fn native_touppercase(args: Vec<Value>) -> Result<Value, RuntimeError> {
check_arg_count("touppercase", &args, 1)?;

let text = expect_text(&args[0])?;
let uppercase = text.to_uppercase();
Ok(Value::Text(Arc::from(uppercase)))
unary_text_op("touppercase", args, |s| s.to_uppercase())
}

pub fn native_tolowercase(args: Vec<Value>) -> Result<Value, RuntimeError> {
check_arg_count("tolowercase", &args, 1)?;

let text = expect_text(&args[0])?;
let lowercase = text.to_lowercase();
Ok(Value::Text(Arc::from(lowercase)))
unary_text_op("tolowercase", args, |s| s.to_lowercase())
}

pub fn native_substring(args: Vec<Value>) -> Result<Value, RuntimeError> {
Expand Down Expand Up @@ -135,29 +129,15 @@ pub fn native_string_split(args: Vec<Value>) -> Result<Value, RuntimeError> {
}

pub fn native_trim(args: Vec<Value>) -> Result<Value, RuntimeError> {
check_arg_count("trim", &args, 1)?;

let text = expect_text(&args[0])?;
let trimmed = text.trim();
Ok(Value::Text(Arc::from(trimmed)))
unary_text_op("trim", args, |s| s.trim().to_string())

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trim previously could avoid constructing an intermediate String by directly converting the trimmed &str into Arc<str>. With unary_text_op requiring Fn(&str) -> String, trim() now always allocates a String, which is avoidable for slice-based transforms. Consider adjusting unary_text_op to accept something like Cow<'_, str> (borrow for trim, own for to_uppercase/etc.), or add a second helper specialized for Fn(&str) -> &str/borrowed results so trim can remain allocation-minimal.

Suggested change
unary_text_op("trim", args, |s| s.trim().to_string())
check_arg_count("trim", &args, 1)?;
let text = expect_text(&args[0])?;
let trimmed = text.trim();
Ok(Value::Text(Arc::from(trimmed)))

Copilot uses AI. Check for mistakes.
}

pub fn native_starts_with(args: Vec<Value>) -> Result<Value, RuntimeError> {
check_arg_count("starts_with", &args, 2)?;

let text = expect_text(&args[0])?;
let prefix = expect_text(&args[1])?;
let result = text.starts_with(prefix.as_ref());
Ok(Value::Bool(result))
binary_text_predicate("starts_with", args, |s, p| s.starts_with(p))
}

pub fn native_ends_with(args: Vec<Value>) -> Result<Value, RuntimeError> {
check_arg_count("ends_with", &args, 2)?;

let text = expect_text(&args[0])?;
let suffix = expect_text(&args[1])?;
let result = text.ends_with(suffix.as_ref());
Ok(Value::Bool(result))
binary_text_predicate("ends_with", args, |s, p| s.ends_with(p))
}

/// Parse query string into WFL object
Expand Down Expand Up @@ -278,26 +258,20 @@ pub fn native_padright(args: Vec<Value>) -> Result<Value, RuntimeError> {
}

pub fn native_capitalize(args: Vec<Value>) -> Result<Value, RuntimeError> {
check_arg_count("capitalize", &args, 1)?;

let text = expect_text(&args[0])?;
let mut chars = text.chars();
let result = match chars.next() {
Some(c) => {
let upper: String = c.to_uppercase().collect();
format!("{}{}", upper, chars.as_str())
unary_text_op("capitalize", args, |text| {
let mut chars = text.chars();
match chars.next() {
Some(c) => {
let upper: String = c.to_uppercase().collect();
format!("{}{}", upper, chars.as_str())
}
None => String::new(),
}
None => String::new(),
};
Ok(Value::Text(Arc::from(result)))
})
}

pub fn native_reverse_text(args: Vec<Value>) -> Result<Value, RuntimeError> {
check_arg_count("reverse", &args, 1)?;

let text = expect_text(&args[0])?;
let reversed: String = text.chars().rev().collect();
Ok(Value::Text(Arc::from(reversed)))
unary_text_op("reverse", args, |text| text.chars().rev().collect())
}

pub fn register_text(env: &mut Environment) {
Expand Down
Loading