From a9c45307ddc179b49f1021731cd4ee66e9ea4a18 Mon Sep 17 00:00:00 2001 From: Quest Date: Fri, 5 Apr 2019 23:07:54 +0200 Subject: [PATCH 1/6] Support addition and comparison of strings. --- src/error/display.rs | 3 +++ src/error/mod.rs | 20 +++++++++++++++ src/operator/mod.rs | 59 +++++++++++++++++++++++++++++++++----------- tests/integration.rs | 3 +++ 4 files changed, 70 insertions(+), 15 deletions(-) diff --git a/src/error/display.rs b/src/error/display.rs index 3bfd640..6ad82f0 100644 --- a/src/error/display.rs +++ b/src/error/display.rs @@ -24,6 +24,9 @@ impl fmt::Display for EvalexprError { ExpectedNumber { actual } => { write!(f, "Expected a Value::Number, but got {:?}.", actual) }, + ExpectedNumberOrString { actual } => { + write!(f, "Expected a Value::Number or a Value::String, but got {:?}.", actual) + }, ExpectedBoolean { actual } => { write!(f, "Expected a Value::Boolean, but got {:?}.", actual) }, diff --git a/src/error/mod.rs b/src/error/mod.rs index b4d9ef9..3956d92 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -56,6 +56,13 @@ pub enum EvalexprError { actual: Value, }, + /// A numeric or string value was expected. + /// Numeric values are the variants `Value::Int` and `Value::Float`. + ExpectedNumberOrString { + /// The actual value. + actual: Value, + }, + /// A boolean value was expected. ExpectedBoolean { /// The actual value. @@ -204,6 +211,11 @@ impl EvalexprError { EvalexprError::ExpectedNumber { actual } } + /// Constructs `Error::ExpectedNumberOrString{actual}`. + pub fn expected_number_or_string(actual: Value) -> Self { + EvalexprError::ExpectedNumberOrString { actual } + } + /// Constructs `Error::ExpectedBoolean{actual}`. pub fn expected_boolean(actual: Value) -> Self { EvalexprError::ExpectedBoolean { actual } @@ -315,6 +327,14 @@ pub fn expect_number(actual: &Value) -> EvalexprResult<()> { } } +/// Returns Ok(()) if the given value is a string or a numeric +pub fn expect_number_or_string(actual: &Value) -> EvalexprResult<()> { + match actual { + Value::String(_) | Value::Float(_) | Value::Int(_) => Ok(()), + _ => Err(EvalexprError::expected_number_or_string(actual.clone())), + } +} + /// Returns `Ok(bool)` if the given value is a `Value::Boolean`, or `Err(Error::ExpectedBoolean)` otherwise. pub fn expect_boolean(actual: &Value) -> EvalexprResult { match actual { diff --git a/src/operator/mod.rs b/src/operator/mod.rs index 4252854..f8b772e 100644 --- a/src/operator/mod.rs +++ b/src/operator/mod.rs @@ -146,10 +146,15 @@ impl Operator for Add { fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult { expect_operator_argument_amount(arguments.len(), 2)?; - expect_number(&arguments[0])?; - expect_number(&arguments[1])?; + expect_number_or_string(&arguments[0])?; + expect_number_or_string(&arguments[1])?; - if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { + if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) { + let mut result = String::with_capacity(a.len() + b.len()); + result.push_str(&a); + result.push_str(&b); + Ok(Value::String(result)) + } else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { let result = a.checked_add(b); if let Some(result) = result { Ok(Value::Int(result)) @@ -395,10 +400,16 @@ impl Operator for Gt { fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult { expect_operator_argument_amount(arguments.len(), 2)?; - expect_number(&arguments[0])?; - expect_number(&arguments[1])?; + expect_number_or_string(&arguments[0])?; + expect_number_or_string(&arguments[1])?; - if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { + if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) { + if a > b { + Ok(Value::Boolean(true)) + } else { + Ok(Value::Boolean(false)) + } + } else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { if a > b { Ok(Value::Boolean(true)) } else { @@ -425,10 +436,16 @@ impl Operator for Lt { fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult { expect_operator_argument_amount(arguments.len(), 2)?; - expect_number(&arguments[0])?; - expect_number(&arguments[1])?; + expect_number_or_string(&arguments[0])?; + expect_number_or_string(&arguments[1])?; - if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { + if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) { + if a < b { + Ok(Value::Boolean(true)) + } else { + Ok(Value::Boolean(false)) + } + } else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { if a < b { Ok(Value::Boolean(true)) } else { @@ -455,10 +472,16 @@ impl Operator for Geq { fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult { expect_operator_argument_amount(arguments.len(), 2)?; - expect_number(&arguments[0])?; - expect_number(&arguments[1])?; + expect_number_or_string(&arguments[0])?; + expect_number_or_string(&arguments[1])?; - if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { + if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) { + if a >= b { + Ok(Value::Boolean(true)) + } else { + Ok(Value::Boolean(false)) + } + } else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { if a >= b { Ok(Value::Boolean(true)) } else { @@ -485,10 +508,16 @@ impl Operator for Leq { fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult { expect_operator_argument_amount(arguments.len(), 2)?; - expect_number(&arguments[0])?; - expect_number(&arguments[1])?; + expect_number_or_string(&arguments[0])?; + expect_number_or_string(&arguments[1])?; - if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { + if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) { + if a <= b { + Ok(Value::Boolean(true)) + } else { + Ok(Value::Boolean(false)) + } + } else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { if a <= b { Ok(Value::Boolean(true)) } else { diff --git a/tests/integration.rs b/tests/integration.rs index 42fa35c..e0e225f 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -551,6 +551,9 @@ fn test_strings() { eval_boolean_with_context("a == \"a string\"", &context), Ok(true) ); + assert_eq!(eval("\"a\" + \"b\""), Ok(Value::from("ab"))); + assert_eq!(eval("\"a\" > \"b\""), Ok(Value::from(false))); + assert_eq!(eval("\"a\" < \"b\""), Ok(Value::from(true))); } #[cfg(feature = "serde")] From be54931f76c73ae1a2c89a293aca84d8cd1f64f7 Mon Sep 17 00:00:00 2001 From: Quest Date: Sun, 7 Apr 2019 08:10:36 +0200 Subject: [PATCH 2/6] Builtin string functions - downcase, len, match(regex), replace(regex), trim, upcase. New dependency regex. --- Cargo.toml | 2 ++ README.md | 6 ++++ src/error/display.rs | 1 + src/error/mod.rs | 13 +++++++ src/function/builtin.rs | 75 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 ++ tests/integration.rs | 51 ++++++++++++++++++++++++++-- 7 files changed, 148 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a0fed32..d6df209 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,11 +16,13 @@ name = "evalexpr" path = "src/lib.rs" [dependencies] +regex = { version = "1", optional = true} serde = { version = "1", optional = true} serde_derive = { version = "1", optional = true} [features] serde_support = ["serde", "serde_derive"] +regex_support = ["regex"] [dev-dependencies] ron = "0.4" diff --git a/README.md b/README.md index 02ddeb4..f8e2976 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,12 @@ This crate offers a set of builtin functions. |------------|-----------------|-------------| | min | >= 1 | Returns the minimum of the arguments | | max | >= 1 | Returns the maximum of the arguments | +| downcase | 1 | Returns lower-case version of string | +| len | 1 | Return the character length of string argument | +| match | 2 | Returns true if first string argument matches regex in second | +| replace | 3 | Returns string with matches replaced by third argument | +| trim | 1 | Strips whitespace from start and end of string | +| upcase | 1 | Returns upper-case version of string | The `min` and `max` functions can deal with a mixture of integer and floating point arguments. They return the result as the type it was passed into the function. diff --git a/src/error/display.rs b/src/error/display.rs index 6ad82f0..589af9e 100644 --- a/src/error/display.rs +++ b/src/error/display.rs @@ -84,6 +84,7 @@ impl fmt::Display for EvalexprError { ModulationError { dividend, divisor } => { write!(f, "Error modulating {} % {}", dividend, divisor) }, + InvalidRegex { regex, message } => write!(f, "Regular expression {} is invalid: {}", regex, message), ContextNotManipulable => write!(f, "Cannot manipulate context"), IllegalEscapeSequence(string) => write!(f, "Illegal escape sequence: {}", string), CustomMessage(message) => write!(f, "Error: {}", message), diff --git a/src/error/mod.rs b/src/error/mod.rs index 3956d92..7eab199 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -167,6 +167,14 @@ pub enum EvalexprError { divisor: Value, }, + /// This regular expression could not be parsed + InvalidRegex { + /// The invalid regular expression + regex: String, + /// Failure message from the regex engine + message: String, + }, + /// A modification was attempted on a `Context` that does not allow modifications. ContextNotManipulable, @@ -279,6 +287,11 @@ impl EvalexprError { pub(crate) fn modulation_error(dividend: Value, divisor: Value) -> Self { EvalexprError::ModulationError { dividend, divisor } } + + /// Constructs `EvalexprError::InvalidRegex(regex)` + pub fn invalid_regex(regex: String, message: String) -> Self { + EvalexprError::InvalidRegex{ regex, message } + } } /// Returns `Ok(())` if the actual and expected parameters are equal, and `Err(Error::WrongOperatorArgumentAmount)` otherwise. diff --git a/src/function/builtin.rs b/src/function/builtin.rs index 2a3d352..358bf28 100644 --- a/src/function/builtin.rs +++ b/src/function/builtin.rs @@ -1,8 +1,27 @@ +#[cfg(feature = "regex_support")] +use regex::Regex; + +use crate::error::*; use value::{FloatType, IntType}; use EvalexprError; use Function; use Value; +#[cfg(feature = "regex_support")] +fn regex_with_local_errors(re_str: &str) -> Result { + match Regex::new(re_str) { + Ok(re) => Ok(re), + Err(regex::Error::Syntax(message)) => + Err(EvalexprError::invalid_regex(re_str.to_string(), message)), + Err(regex::Error::CompiledTooBig(max_size)) => + Err(EvalexprError::invalid_regex( + re_str.to_string(), + format!("Regex exceeded max size {}", max_size)) + ), + Err(err) => Err(EvalexprError::CustomMessage(err.to_string())), + } +} + pub fn builtin_function(identifier: &str) -> Option { match identifier { "min" => Some(Function::new( @@ -53,6 +72,62 @@ pub fn builtin_function(identifier: &str) -> Option { } }), )), + + // string functions + + "downcase" => Some(Function::new( + Some(1), + Box::new(|arguments| { + let subject = expect_string(&arguments[0])?; + Ok(Value::from(subject.to_lowercase())) + }), + )), + "len" => Some(Function::new( + Some(1), + Box::new(|arguments| { + let subject = expect_string(&arguments[0])?; + Ok(Value::from(subject.len() as i64)) + }), + )), + #[cfg(feature = "regex_support")] + "match" => Some(Function::new( + Some(2), + Box::new(|arguments| { + let subject = expect_string(&arguments[0])?; + let re_str = expect_string(&arguments[1])?; + match regex_with_local_errors(re_str) { + Ok(re) => Ok(Value::Boolean(re.is_match(subject))), + Err(err) => Err(err) + } + }), + )), + #[cfg(feature = "regex_support")] + "replace" => Some(Function::new( + Some(3), + Box::new(|arguments| { + let subject = expect_string(&arguments[0])?; + let re_str = expect_string(&arguments[1])?; + let repl = expect_string(&arguments[2])?; + match regex_with_local_errors(re_str) { + Ok(re) => Ok(Value::String(re.replace_all(subject, repl).to_string())), + Err(err) => Err(err), + } + }), + )), + "trim" => Some(Function::new( + Some(1), + Box::new(|arguments| { + let subject = expect_string(&arguments[0])?; + Ok(Value::from(subject.trim())) + }), + )), + "upcase" => Some(Function::new( + Some(1), + Box::new(|arguments| { + let subject = expect_string(&arguments[0])?; + Ok(Value::from(subject.to_uppercase())) + }), + )), _ => None, } } diff --git a/src/lib.rs b/src/lib.rs index 73cab86..42ce7ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -347,6 +347,8 @@ #![warn(missing_docs)] +#[cfg(feature = "regex_support")] +extern crate regex; #[cfg(test)] extern crate ron; #[cfg(feature = "serde_support")] diff --git a/tests/integration.rs b/tests/integration.rs index e0e225f..01b9a0d 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -275,15 +275,62 @@ fn test_n_ary_functions() { Ok(Value::Int(3)) ); assert_eq!(eval_with_context("count 5", &context), Ok(Value::Int(1))); +} +#[test] +fn test_builtin_functions() { assert_eq!( - eval_with_context("min(4.0, 3)", &context), + eval("min(4.0, 3)"), Ok(Value::Int(3)) ); assert_eq!( - eval_with_context("max(4.0, 3)", &context), + eval("max(4.0, 3)"), Ok(Value::Float(4.0)) ); + assert_eq!( + eval("downcase(\"FOOBAR\")"), + Ok(Value::from("foobar")) + ); + assert_eq!( + eval("len(\"foobar\")"), + Ok(Value::Int(6)) + ); + assert_eq!( + eval("trim(\" foo bar \")"), + Ok(Value::from("foo bar")) + ); + assert_eq!( + eval("upcase(\"foobar\")"), + Ok(Value::from("FOOBAR")) + ); +} + +#[test] +#[cfg(feature = "regex_support")] +fn test_regex_functions() { + assert_eq!( + eval("match(\"foobar\", \"[ob]{3}\")"), + Ok(Value::Boolean(true)) + ); + assert_eq!( + eval("match(\"gazonk\", \"[ob]{3}\")"), + Ok(Value::Boolean(false)) + ); + match eval("match(\"foo\", \"[\")") { + Err(EvalexprError::InvalidRegex{ regex, message }) => { + assert_eq!(regex, "["); + assert!(message.contains("unclosed character class")); + }, + v => panic!(v), + }; + assert_eq!( + eval("replace(\"foobar\", \".*?(o+)\", \"b$1\")"), + Ok(Value::String("boobar".to_owned())) + ); + assert_eq!( + eval("replace(\"foobar\", \".*?(i+)\", \"b$1\")"), + Ok(Value::String("foobar".to_owned())) + ); } #[test] From f6c8689303d0e3b246cafee1dd06dbc1fb5d4876 Mon Sep 17 00:00:00 2001 From: Quest Date: Fri, 12 Apr 2019 23:45:07 +0200 Subject: [PATCH 3/6] fixup! Builtin string functions - downcase, len, match(regex), replace(regex), trim, upcase. New dependency regex. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f8e2976..414f0e9 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,8 @@ This crate offers a set of builtin functions. | upcase | 1 | Returns upper-case version of string | The `min` and `max` functions can deal with a mixture of integer and floating point arguments. -They return the result as the type it was passed into the function. +They return the result as the type it was passed into the function. The regex functions require +feature flag `regex_support`. ### Values From 7c6b6d4319fd088e60b69a79a0c457feb9b08454 Mon Sep 17 00:00:00 2001 From: Quest Date: Fri, 12 Apr 2019 23:03:13 +0200 Subject: [PATCH 4/6] Rename matches,replace -> regex_{matches,replace}. Also simplify their error handling. --- README.md | 4 ++-- src/error/display.rs | 2 +- src/error/mod.rs | 2 +- src/function/builtin.rs | 27 ++++++--------------------- tests/integration.rs | 10 +++++----- 5 files changed, 15 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 414f0e9..46a113f 100644 --- a/README.md +++ b/README.md @@ -216,8 +216,8 @@ This crate offers a set of builtin functions. | max | >= 1 | Returns the maximum of the arguments | | downcase | 1 | Returns lower-case version of string | | len | 1 | Return the character length of string argument | -| match | 2 | Returns true if first string argument matches regex in second | -| replace | 3 | Returns string with matches replaced by third argument | +| regex_matches | 2 | Returns true if first string argument matches regex in second | +| regex_replace | 3 | Returns string with matches replaced by third argument | | trim | 1 | Strips whitespace from start and end of string | | upcase | 1 | Returns upper-case version of string | diff --git a/src/error/display.rs b/src/error/display.rs index 589af9e..496f787 100644 --- a/src/error/display.rs +++ b/src/error/display.rs @@ -84,7 +84,7 @@ impl fmt::Display for EvalexprError { ModulationError { dividend, divisor } => { write!(f, "Error modulating {} % {}", dividend, divisor) }, - InvalidRegex { regex, message } => write!(f, "Regular expression {} is invalid: {}", regex, message), + InvalidRegex { regex, message } => write!(f, "Regular expression {:?} is invalid: {:?}", regex, message), ContextNotManipulable => write!(f, "Cannot manipulate context"), IllegalEscapeSequence(string) => write!(f, "Illegal escape sequence: {}", string), CustomMessage(message) => write!(f, "Error: {}", message), diff --git a/src/error/mod.rs b/src/error/mod.rs index 7eab199..729f192 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -167,7 +167,7 @@ pub enum EvalexprError { divisor: Value, }, - /// This regular expression could not be parsed + /// A regular expression could not be parsed InvalidRegex { /// The invalid regular expression regex: String, diff --git a/src/function/builtin.rs b/src/function/builtin.rs index 358bf28..e0d7a7d 100644 --- a/src/function/builtin.rs +++ b/src/function/builtin.rs @@ -7,21 +7,6 @@ use EvalexprError; use Function; use Value; -#[cfg(feature = "regex_support")] -fn regex_with_local_errors(re_str: &str) -> Result { - match Regex::new(re_str) { - Ok(re) => Ok(re), - Err(regex::Error::Syntax(message)) => - Err(EvalexprError::invalid_regex(re_str.to_string(), message)), - Err(regex::Error::CompiledTooBig(max_size)) => - Err(EvalexprError::invalid_regex( - re_str.to_string(), - format!("Regex exceeded max size {}", max_size)) - ), - Err(err) => Err(EvalexprError::CustomMessage(err.to_string())), - } -} - pub fn builtin_function(identifier: &str) -> Option { match identifier { "min" => Some(Function::new( @@ -90,27 +75,27 @@ pub fn builtin_function(identifier: &str) -> Option { }), )), #[cfg(feature = "regex_support")] - "match" => Some(Function::new( + "regex_matches" => Some(Function::new( Some(2), Box::new(|arguments| { let subject = expect_string(&arguments[0])?; let re_str = expect_string(&arguments[1])?; - match regex_with_local_errors(re_str) { + match Regex::new(re_str) { Ok(re) => Ok(Value::Boolean(re.is_match(subject))), - Err(err) => Err(err) + Err(err) => Err(EvalexprError::invalid_regex(re_str.to_string(), format!("{}", err))) } }), )), #[cfg(feature = "regex_support")] - "replace" => Some(Function::new( + "regex_replace" => Some(Function::new( Some(3), Box::new(|arguments| { let subject = expect_string(&arguments[0])?; let re_str = expect_string(&arguments[1])?; let repl = expect_string(&arguments[2])?; - match regex_with_local_errors(re_str) { + match Regex::new(re_str) { Ok(re) => Ok(Value::String(re.replace_all(subject, repl).to_string())), - Err(err) => Err(err), + Err(err) => Err(EvalexprError::invalid_regex(re_str.to_string(), format!("{}", err))), } }), )), diff --git a/tests/integration.rs b/tests/integration.rs index 01b9a0d..13f0793 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -309,14 +309,14 @@ fn test_builtin_functions() { #[cfg(feature = "regex_support")] fn test_regex_functions() { assert_eq!( - eval("match(\"foobar\", \"[ob]{3}\")"), + eval("regex_matches(\"foobar\", \"[ob]{3}\")"), Ok(Value::Boolean(true)) ); assert_eq!( - eval("match(\"gazonk\", \"[ob]{3}\")"), + eval("regex_matches(\"gazonk\", \"[ob]{3}\")"), Ok(Value::Boolean(false)) ); - match eval("match(\"foo\", \"[\")") { + match eval("regex_matches(\"foo\", \"[\")") { Err(EvalexprError::InvalidRegex{ regex, message }) => { assert_eq!(regex, "["); assert!(message.contains("unclosed character class")); @@ -324,11 +324,11 @@ fn test_regex_functions() { v => panic!(v), }; assert_eq!( - eval("replace(\"foobar\", \".*?(o+)\", \"b$1\")"), + eval("regex_replace(\"foobar\", \".*?(o+)\", \"b$1\")"), Ok(Value::String("boobar".to_owned())) ); assert_eq!( - eval("replace(\"foobar\", \".*?(i+)\", \"b$1\")"), + eval("regex_replace(\"foobar\", \".*?(i+)\", \"b$1\")"), Ok(Value::String("foobar".to_owned())) ); } From d72334c246665b49eec06e7e6237798693b7d90c Mon Sep 17 00:00:00 2001 From: Quest Date: Fri, 12 Apr 2019 23:04:40 +0200 Subject: [PATCH 5/6] Rename builtin upcase,downcase -> to_uppercase, to_lowercase. --- README.md | 4 ++-- src/error/mod.rs | 2 +- src/function/builtin.rs | 4 ++-- tests/integration.rs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 46a113f..7448d3d 100644 --- a/README.md +++ b/README.md @@ -214,12 +214,12 @@ This crate offers a set of builtin functions. |------------|-----------------|-------------| | min | >= 1 | Returns the minimum of the arguments | | max | >= 1 | Returns the maximum of the arguments | -| downcase | 1 | Returns lower-case version of string | +| to_lowercase | 1 | Returns lower-case version of string | | len | 1 | Return the character length of string argument | | regex_matches | 2 | Returns true if first string argument matches regex in second | | regex_replace | 3 | Returns string with matches replaced by third argument | | trim | 1 | Strips whitespace from start and end of string | -| upcase | 1 | Returns upper-case version of string | +| to_uppercase | 1 | Returns upper-case version of string | The `min` and `max` functions can deal with a mixture of integer and floating point arguments. They return the result as the type it was passed into the function. The regex functions require diff --git a/src/error/mod.rs b/src/error/mod.rs index 729f192..47d5bb1 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -340,7 +340,7 @@ pub fn expect_number(actual: &Value) -> EvalexprResult<()> { } } -/// Returns Ok(()) if the given value is a string or a numeric +/// Returns `Ok(())` if the given value is a string or a numeric pub fn expect_number_or_string(actual: &Value) -> EvalexprResult<()> { match actual { Value::String(_) | Value::Float(_) | Value::Int(_) => Ok(()), diff --git a/src/function/builtin.rs b/src/function/builtin.rs index e0d7a7d..38ccb56 100644 --- a/src/function/builtin.rs +++ b/src/function/builtin.rs @@ -60,7 +60,7 @@ pub fn builtin_function(identifier: &str) -> Option { // string functions - "downcase" => Some(Function::new( + "to_lowercase" => Some(Function::new( Some(1), Box::new(|arguments| { let subject = expect_string(&arguments[0])?; @@ -106,7 +106,7 @@ pub fn builtin_function(identifier: &str) -> Option { Ok(Value::from(subject.trim())) }), )), - "upcase" => Some(Function::new( + "to_uppercase" => Some(Function::new( Some(1), Box::new(|arguments| { let subject = expect_string(&arguments[0])?; diff --git a/tests/integration.rs b/tests/integration.rs index 13f0793..6d308b7 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -288,7 +288,7 @@ fn test_builtin_functions() { Ok(Value::Float(4.0)) ); assert_eq!( - eval("downcase(\"FOOBAR\")"), + eval("to_lowercase(\"FOOBAR\")"), Ok(Value::from("foobar")) ); assert_eq!( @@ -300,7 +300,7 @@ fn test_builtin_functions() { Ok(Value::from("foo bar")) ); assert_eq!( - eval("upcase(\"foobar\")"), + eval("to_uppercase(\"foobar\")"), Ok(Value::from("FOOBAR")) ); } From dbf3949af62f44814e70a1f2f226ec592d331833 Mon Sep 17 00:00:00 2001 From: Quest Date: Fri, 12 Apr 2019 23:13:37 +0200 Subject: [PATCH 6/6] Namespace string builtins into "str::". --- README.md | 10 +++++----- src/function/builtin.rs | 29 +++++++++++++++-------------- tests/integration.rs | 24 ++++++++++++------------ 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 7448d3d..9421533 100644 --- a/README.md +++ b/README.md @@ -214,12 +214,12 @@ This crate offers a set of builtin functions. |------------|-----------------|-------------| | min | >= 1 | Returns the minimum of the arguments | | max | >= 1 | Returns the maximum of the arguments | -| to_lowercase | 1 | Returns lower-case version of string | | len | 1 | Return the character length of string argument | -| regex_matches | 2 | Returns true if first string argument matches regex in second | -| regex_replace | 3 | Returns string with matches replaced by third argument | -| trim | 1 | Strips whitespace from start and end of string | -| to_uppercase | 1 | Returns upper-case version of string | +| str::regex_matches | 2 | Returns true if first string argument matches regex in second | +| str::regex_replace | 3 | Returns string with matches replaced by third argument | +| str::to_lowercase | 1 | Returns lower-case version of string | +| str::to_uppercase | 1 | Returns upper-case version of string | +| str::trim | 1 | Strips whitespace from start and end of string | The `min` and `max` functions can deal with a mixture of integer and floating point arguments. They return the result as the type it was passed into the function. The regex functions require diff --git a/src/function/builtin.rs b/src/function/builtin.rs index 38ccb56..717b7db 100644 --- a/src/function/builtin.rs +++ b/src/function/builtin.rs @@ -58,15 +58,6 @@ pub fn builtin_function(identifier: &str) -> Option { }), )), - // string functions - - "to_lowercase" => Some(Function::new( - Some(1), - Box::new(|arguments| { - let subject = expect_string(&arguments[0])?; - Ok(Value::from(subject.to_lowercase())) - }), - )), "len" => Some(Function::new( Some(1), Box::new(|arguments| { @@ -74,8 +65,11 @@ pub fn builtin_function(identifier: &str) -> Option { Ok(Value::from(subject.len() as i64)) }), )), + + // string functions + #[cfg(feature = "regex_support")] - "regex_matches" => Some(Function::new( + "str::regex_matches" => Some(Function::new( Some(2), Box::new(|arguments| { let subject = expect_string(&arguments[0])?; @@ -87,7 +81,7 @@ pub fn builtin_function(identifier: &str) -> Option { }), )), #[cfg(feature = "regex_support")] - "regex_replace" => Some(Function::new( + "str::regex_replace" => Some(Function::new( Some(3), Box::new(|arguments| { let subject = expect_string(&arguments[0])?; @@ -99,20 +93,27 @@ pub fn builtin_function(identifier: &str) -> Option { } }), )), - "trim" => Some(Function::new( + "str::to_lowercase" => Some(Function::new( Some(1), Box::new(|arguments| { let subject = expect_string(&arguments[0])?; - Ok(Value::from(subject.trim())) + Ok(Value::from(subject.to_lowercase())) }), )), - "to_uppercase" => Some(Function::new( + "str::to_uppercase" => Some(Function::new( Some(1), Box::new(|arguments| { let subject = expect_string(&arguments[0])?; Ok(Value::from(subject.to_uppercase())) }), )), + "str::trim" => Some(Function::new( + Some(1), + Box::new(|arguments| { + let subject = expect_string(&arguments[0])?; + Ok(Value::from(subject.trim())) + }), + )), _ => None, } } diff --git a/tests/integration.rs b/tests/integration.rs index 6d308b7..2c90c9f 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -287,36 +287,36 @@ fn test_builtin_functions() { eval("max(4.0, 3)"), Ok(Value::Float(4.0)) ); - assert_eq!( - eval("to_lowercase(\"FOOBAR\")"), - Ok(Value::from("foobar")) - ); assert_eq!( eval("len(\"foobar\")"), Ok(Value::Int(6)) ); assert_eq!( - eval("trim(\" foo bar \")"), - Ok(Value::from("foo bar")) + eval("str::to_lowercase(\"FOOBAR\")"), + Ok(Value::from("foobar")) ); assert_eq!( - eval("to_uppercase(\"foobar\")"), + eval("str::to_uppercase(\"foobar\")"), Ok(Value::from("FOOBAR")) ); + assert_eq!( + eval("str::trim(\" foo bar \")"), + Ok(Value::from("foo bar")) + ); } #[test] #[cfg(feature = "regex_support")] fn test_regex_functions() { assert_eq!( - eval("regex_matches(\"foobar\", \"[ob]{3}\")"), + eval("str::regex_matches(\"foobar\", \"[ob]{3}\")"), Ok(Value::Boolean(true)) ); assert_eq!( - eval("regex_matches(\"gazonk\", \"[ob]{3}\")"), + eval("str::regex_matches(\"gazonk\", \"[ob]{3}\")"), Ok(Value::Boolean(false)) ); - match eval("regex_matches(\"foo\", \"[\")") { + match eval("str::regex_matches(\"foo\", \"[\")") { Err(EvalexprError::InvalidRegex{ regex, message }) => { assert_eq!(regex, "["); assert!(message.contains("unclosed character class")); @@ -324,11 +324,11 @@ fn test_regex_functions() { v => panic!(v), }; assert_eq!( - eval("regex_replace(\"foobar\", \".*?(o+)\", \"b$1\")"), + eval("str::regex_replace(\"foobar\", \".*?(o+)\", \"b$1\")"), Ok(Value::String("boobar".to_owned())) ); assert_eq!( - eval("regex_replace(\"foobar\", \".*?(i+)\", \"b$1\")"), + eval("str::regex_replace(\"foobar\", \".*?(i+)\", \"b$1\")"), Ok(Value::String("foobar".to_owned())) ); }