diff --git a/Cargo.toml b/Cargo.toml index 38db883..6601ba7 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..9421533 100644 --- a/README.md +++ b/README.md @@ -214,9 +214,16 @@ This crate offers a set of builtin functions. |------------|-----------------|-------------| | min | >= 1 | Returns the minimum of the arguments | | max | >= 1 | Returns the maximum of the arguments | +| len | 1 | Return the character length of string argument | +| 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. +They return the result as the type it was passed into the function. The regex functions require +feature flag `regex_support`. ### Values diff --git a/src/error/display.rs b/src/error/display.rs index a8c3710..d0ac2d2 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::Float or Value::Int, 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) }, @@ -81,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 b4d9ef9..47d5bb1 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. @@ -160,6 +167,14 @@ pub enum EvalexprError { divisor: Value, }, + /// A 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, @@ -204,6 +219,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 } @@ -267,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. @@ -315,6 +340,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/function/builtin.rs b/src/function/builtin.rs index 2a3d352..717b7db 100644 --- a/src/function/builtin.rs +++ b/src/function/builtin.rs @@ -1,3 +1,7 @@ +#[cfg(feature = "regex_support")] +use regex::Regex; + +use crate::error::*; use value::{FloatType, IntType}; use EvalexprError; use Function; @@ -53,6 +57,63 @@ pub fn builtin_function(identifier: &str) -> Option { } }), )), + + "len" => Some(Function::new( + Some(1), + Box::new(|arguments| { + let subject = expect_string(&arguments[0])?; + Ok(Value::from(subject.len() as i64)) + }), + )), + + // string functions + + #[cfg(feature = "regex_support")] + "str::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::new(re_str) { + Ok(re) => Ok(Value::Boolean(re.is_match(subject))), + Err(err) => Err(EvalexprError::invalid_regex(re_str.to_string(), format!("{}", err))) + } + }), + )), + #[cfg(feature = "regex_support")] + "str::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::new(re_str) { + Ok(re) => Ok(Value::String(re.replace_all(subject, repl).to_string())), + Err(err) => Err(EvalexprError::invalid_regex(re_str.to_string(), format!("{}", err))), + } + }), + )), + "str::to_lowercase" => Some(Function::new( + Some(1), + Box::new(|arguments| { + let subject = expect_string(&arguments[0])?; + Ok(Value::from(subject.to_lowercase())) + }), + )), + "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/src/lib.rs b/src/lib.rs index ec8c1b6..1619b93 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/src/operator/mod.rs b/src/operator/mod.rs index 1f6e7a0..e367546 100644 --- a/src/operator/mod.rs +++ b/src/operator/mod.rs @@ -151,10 +151,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)) @@ -400,10 +405,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 { @@ -430,10 +441,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 { @@ -460,10 +477,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 { @@ -490,10 +513,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..2c90c9f 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("len(\"foobar\")"), + Ok(Value::Int(6)) + ); + assert_eq!( + eval("str::to_lowercase(\"FOOBAR\")"), + Ok(Value::from("foobar")) + ); + assert_eq!( + 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("str::regex_matches(\"foobar\", \"[ob]{3}\")"), + Ok(Value::Boolean(true)) + ); + assert_eq!( + eval("str::regex_matches(\"gazonk\", \"[ob]{3}\")"), + Ok(Value::Boolean(false)) + ); + match eval("str::regex_matches(\"foo\", \"[\")") { + Err(EvalexprError::InvalidRegex{ regex, message }) => { + assert_eq!(regex, "["); + assert!(message.contains("unclosed character class")); + }, + v => panic!(v), + }; + assert_eq!( + eval("str::regex_replace(\"foobar\", \".*?(o+)\", \"b$1\")"), + Ok(Value::String("boobar".to_owned())) + ); + assert_eq!( + eval("str::regex_replace(\"foobar\", \".*?(i+)\", \"b$1\")"), + Ok(Value::String("foobar".to_owned())) + ); } #[test] @@ -551,6 +598,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")]