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]