From 53c3b0a413704087d9721269c009f81fc10511e0 Mon Sep 17 00:00:00 2001 From: Alex Snaps Date: Wed, 19 Jun 2024 08:21:43 -0400 Subject: [PATCH 1/2] Added support for .matches macro on String --- interpreter/Cargo.toml | 1 + interpreter/src/context.rs | 1 + interpreter/src/functions.rs | 12 ++++++++++++ interpreter/src/lib.rs | 21 +++++++++++++++++++++ interpreter/src/objects.rs | 1 - 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/interpreter/Cargo.toml b/interpreter/Cargo.toml index 655bd7d..c62c854 100644 --- a/interpreter/Cargo.toml +++ b/interpreter/Cargo.toml @@ -15,6 +15,7 @@ chrono = "0.4.26" nom = "7.1.3" paste = "1.0.14" serde = "1.0.196" +regex = "1.10.5" [dev-dependencies] criterion = { version = "0.5.1", features = ["html_reports"] } diff --git a/interpreter/src/context.rs b/interpreter/src/context.rs index 4c5d270..5761a03 100644 --- a/interpreter/src/context.rs +++ b/interpreter/src/context.rs @@ -154,6 +154,7 @@ impl<'a> Default for Context<'a> { ctx.add_function("all", functions::all); ctx.add_function("max", functions::max); ctx.add_function("startsWith", functions::starts_with); + ctx.add_function("matches", functions::matches); ctx.add_function("duration", functions::duration); ctx.add_function("timestamp", functions::timestamp); ctx.add_function("string", functions::string); diff --git a/interpreter/src/functions.rs b/interpreter/src/functions.rs index 8a24bf4..b36b7b3 100644 --- a/interpreter/src/functions.rs +++ b/interpreter/src/functions.rs @@ -6,6 +6,7 @@ use crate::resolvers::{Argument, Resolver}; use crate::ExecutionError; use cel_parser::Expression; use chrono::{DateTime, Duration, FixedOffset}; +use regex::Regex; use std::cmp::Ordering; use std::convert::TryInto; use std::sync::Arc; @@ -223,6 +224,17 @@ pub fn starts_with(This(this): This>, prefix: Arc) -> bool { this.starts_with(prefix.as_str()) } +/// Returns true if a string matches the regular expression. +/// +/// # Example +/// ```cel +/// "abc".matches("^[a-z]*$") == true +/// ``` +pub fn matches(This(this): This>, regex: Arc) -> bool { + let re = Regex::new(®ex).unwrap(); + re.is_match(&this) +} + /// Returns true if the provided argument can be resolved. This function is /// useful for checking if a property exists on a type before attempting to /// resolve it. Resolving a property that does not exist will result in a diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs index 93a00df..fa5a8ed 100644 --- a/interpreter/src/lib.rs +++ b/interpreter/src/lib.rs @@ -1,6 +1,7 @@ extern crate core; use cel_parser::parse; +use std::collections::HashMap; use std::convert::TryFrom; use std::sync::Arc; use thiserror::Error; @@ -20,6 +21,7 @@ mod resolvers; mod ser; pub use ser::to_value; mod testing; +use crate::testing::test_script; use magic::FromContext; pub mod extractors { @@ -199,6 +201,25 @@ mod tests { } } + #[test] + fn test_matches() { + let tests = vec![ + ("string", "'foobar'.matches('^[a-zA-Z]*$') == true"), + ( + "map", + "{'1': 'abc', '2': 'def', '3': 'ghi'}.all(key, key.matches('^[a-zA-Z]*$')) == false", + ), + ]; + + for (name, script) in tests { + assert_eq!( + test_script(script, None), + Ok(true.into()), + ".matches failed for '{name}'" + ); + } + } + #[test] fn test_execution_errors() { let tests = vec![ diff --git a/interpreter/src/objects.rs b/interpreter/src/objects.rs index 3e7d8be..932fa3f 100644 --- a/interpreter/src/objects.rs +++ b/interpreter/src/objects.rs @@ -860,6 +860,5 @@ mod tests { let program = Program::compile("size == 50").unwrap(); let value = program.execute(&context).unwrap(); assert_eq!(value, false.into()); - } } From 44b498e4b13d5e37cd603c5e760be53fa9f34416 Mon Sep 17 00:00:00 2001 From: Alex Snaps Date: Wed, 19 Jun 2024 10:58:57 -0400 Subject: [PATCH 2/2] Proper err handling --- interpreter/src/functions.rs | 14 ++++++++++---- interpreter/src/lib.rs | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/interpreter/src/functions.rs b/interpreter/src/functions.rs index b36b7b3..6ca5409 100644 --- a/interpreter/src/functions.rs +++ b/interpreter/src/functions.rs @@ -6,7 +6,7 @@ use crate::resolvers::{Argument, Resolver}; use crate::ExecutionError; use cel_parser::Expression; use chrono::{DateTime, Duration, FixedOffset}; -use regex::Regex; +use regex::{Error, Regex}; use std::cmp::Ordering; use std::convert::TryInto; use std::sync::Arc; @@ -230,9 +230,15 @@ pub fn starts_with(This(this): This>, prefix: Arc) -> bool { /// ```cel /// "abc".matches("^[a-z]*$") == true /// ``` -pub fn matches(This(this): This>, regex: Arc) -> bool { - let re = Regex::new(®ex).unwrap(); - re.is_match(&this) +pub fn matches( + ftx: &FunctionContext, + This(this): This>, + regex: Arc, +) -> Result { + match Regex::new(®ex) { + Ok(re) => Ok(re.is_match(&this)), + Err(err) => Err(ftx.error(format!("'{regex}' not a valid regex:\n{err}"))), + } } /// Returns true if the provided argument can be resolved. This function is diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs index fa5a8ed..014fdbd 100644 --- a/interpreter/src/lib.rs +++ b/interpreter/src/lib.rs @@ -142,6 +142,7 @@ mod tests { use crate::context::Context; use crate::objects::{ResolveResult, Value}; use crate::testing::test_script; + use crate::ExecutionError::FunctionError; use crate::{ExecutionError, Program}; use std::collections::HashMap; use std::convert::TryInto; @@ -220,6 +221,20 @@ mod tests { } } + #[test] + fn test_matches_err() { + assert_eq!( + test_script( + "'foobar'.matches('(foo') == true", None), + Err( + FunctionError { + function: "matches".to_string(), + message: "'(foo' not a valid regex:\nregex parse error:\n (foo\n ^\nerror: unclosed group".to_string() + } + ) + ); + } + #[test] fn test_execution_errors() { let tests = vec![