Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

String operators and builtin functions #38

Merged
merged 6 commits into from
Apr 13, 2019
Merged
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions src/error/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
Expand Down Expand Up @@ -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),
Expand Down
33 changes: 33 additions & 0 deletions src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<bool> {
match actual {
Expand Down
61 changes: 61 additions & 0 deletions src/function/builtin.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#[cfg(feature = "regex_support")]
use regex::Regex;

use crate::error::*;
use value::{FloatType, IntType};
use EvalexprError;
use Function;
Expand Down Expand Up @@ -53,6 +57,63 @@ pub fn builtin_function(identifier: &str) -> Option<Function> {
}
}),
)),

"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,
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@

#![warn(missing_docs)]

#[cfg(feature = "regex_support")]
extern crate regex;
#[cfg(test)]
extern crate ron;
#[cfg(feature = "serde_support")]
Expand Down
61 changes: 45 additions & 16 deletions src/operator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,15 @@ impl Operator for Add {

fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
expect_operator_argument_amount(arguments.len(), 2)?;
expect_number(&arguments[0])?;
expect_number(&arguments[1])?;

if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
expect_number_or_string(&arguments[0])?;
expect_number_or_string(&arguments[1])?;

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))
Expand Down Expand Up @@ -395,10 +400,16 @@ impl Operator for Gt {

fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
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 {
Expand All @@ -425,10 +436,16 @@ impl Operator for Lt {

fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
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 {
Expand All @@ -455,10 +472,16 @@ impl Operator for Geq {

fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
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 {
Expand All @@ -485,10 +508,16 @@ impl Operator for Leq {

fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
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 {
Expand Down
54 changes: 52 additions & 2 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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")]
Expand Down