From 760902cce0bfd52396b649e9a38b809fd98c07e2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:57:50 +0000 Subject: [PATCH] Refactor text stdlib to reduce duplication Co-authored-by: logbie <1138960+logbie@users.noreply.github.com> --- src/stdlib/helpers.rs | 90 +++++++++++++++++++++++++++++++++++++ src/stdlib/text.rs | 100 ++++++++++++++++++------------------------ 2 files changed, 132 insertions(+), 58 deletions(-) diff --git a/src/stdlib/helpers.rs b/src/stdlib/helpers.rs index 5dd68395..ffb519cc 100644 --- a/src/stdlib/helpers.rs +++ b/src/stdlib/helpers.rs @@ -196,6 +196,51 @@ pub fn expect_number(value: &Value) -> Result { } } +/// 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(func_name: &str, args: Vec, op: F) -> Result +where + F: Fn(&str) -> R, + R: Into>, +{ + check_arg_count(func_name, &args, 1)?; + let text = expect_text(&args[0])?; + Ok(Value::Text(op(&text).into())) +} + +/// 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 text predicate to perform +pub fn binary_text_predicate( + func_name: &str, + args: Vec, + op: F, +) -> Result +where + F: Fn(&str, &str) -> bool, +{ + check_arg_count(func_name, &args, 2)?; + let text = expect_text(&args[0])?; + let other = expect_text(&args[1])?; + Ok(Value::Bool(op(&text, &other))) +} + + /// Helper for unary math operations (f64 -> f64) /// /// Handles argument count checking, type extraction, operation execution, @@ -473,3 +518,48 @@ pub fn expect_datetime(value: &Value) -> Result, Runti )), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unary_text_op() { + let op = |s: &str| s.to_uppercase(); + let result = unary_text_op("test_op", vec![Value::Text(Arc::from("hello"))], op).unwrap(); + assert_eq!(result, Value::Text(Arc::from("HELLO"))); + } + + #[test] + fn test_unary_text_op_arg_count() { + let op = |s: &str| s.to_uppercase(); + assert!(unary_text_op("test_op", vec![], op).is_err()); + } + + #[test] + fn test_unary_text_op_wrong_type() { + let op = |s: &str| s.to_uppercase(); + assert!(unary_text_op("test_op", vec![Value::Number(1.0)], op).is_err()); + } + + #[test] + fn test_binary_text_predicate() { + let op = |a: &str, b: &str| a.starts_with(b); + let result = binary_text_predicate( + "test_pred", + vec![ + Value::Text(Arc::from("hello world")), + Value::Text(Arc::from("hello")), + ], + op, + ) + .unwrap(); + assert_eq!(result, Value::Bool(true)); + } + + #[test] + fn test_binary_text_predicate_arg_count() { + let op = |a: &str, b: &str| a.starts_with(b); + assert!(binary_text_predicate("test_pred", vec![], op).is_err()); + } +} diff --git a/src/stdlib/text.rs b/src/stdlib/text.rs index b91c428b..d443492d 100644 --- a/src/stdlib/text.rs +++ b/src/stdlib/text.rs @@ -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; @@ -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) -> Result { - 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) -> Result { - 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) -> Result { @@ -135,29 +129,15 @@ pub fn native_string_split(args: Vec) -> Result { } pub fn native_trim(args: Vec) -> Result { - 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| Arc::from(s.trim())) } pub fn native_starts_with(args: Vec) -> Result { - 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, |text, prefix| text.starts_with(prefix)) } pub fn native_ends_with(args: Vec) -> Result { - 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, |text, suffix| text.ends_with(suffix)) } /// Parse query string into WFL object @@ -249,8 +229,17 @@ fn validated_pad_width(raw: f64) -> Result { Ok((raw as usize).min(MAX_PAD_WIDTH)) } -pub fn native_padleft(args: Vec) -> Result { - check_arg_count("padleft", &args, 2)?; +enum PadDirection { + Left, + Right, +} + +fn perform_pad(args: Vec, direction: PadDirection) -> Result { + let func_name = match direction { + PadDirection::Left => "padleft", + PadDirection::Right => "padright", + }; + check_arg_count(func_name, &args, 2)?; let text = expect_text(&args[0])?; let width = validated_pad_width(expect_number(&args[1])?)?; @@ -259,45 +248,40 @@ pub fn native_padleft(args: Vec) -> Result { Ok(Value::Text(Arc::clone(&text))) } else { let padding = " ".repeat(width - len); - Ok(Value::Text(Arc::from(format!("{}{}", padding, text)))) + let result = match direction { + PadDirection::Left => format!("{}{}", padding, text), + PadDirection::Right => format!("{}{}", text, padding), + }; + Ok(Value::Text(Arc::from(result))) } } -pub fn native_padright(args: Vec) -> Result { - check_arg_count("padright", &args, 2)?; +pub fn native_padleft(args: Vec) -> Result { + perform_pad(args, PadDirection::Left) +} - let text = expect_text(&args[0])?; - let width = validated_pad_width(expect_number(&args[1])?)?; - let len = text.chars().count(); - if len >= width { - Ok(Value::Text(Arc::clone(&text))) - } else { - let padding = " ".repeat(width - len); - Ok(Value::Text(Arc::from(format!("{}{}", text, padding)))) - } +pub fn native_padright(args: Vec) -> Result { + perform_pad(args, PadDirection::Right) } pub fn native_capitalize(args: Vec) -> Result { - 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) -> Result { - 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| { + let reversed: String = text.chars().rev().collect(); + reversed + }) } pub fn register_text(env: &mut Environment) {