Repository: clarkmcc/cel-rust (language: rust)

Issue:
Regex support?
This is more of a question really than an actual issue... for now at least. 

If I read the cel spec properly, it is expected it'd support regular expressions, re2 based flavor.

Trying this out:

```rust
#[test]
fn test_matches() {
  let tests = vec![
      ("map", "{1: 'abc', 2: 'def', 3: 'ghi'}.all(key, key.matches('^[a-zA-Z]*$')) == true"),
      ("string", "'foobar'.matches('^[a-zA-Z]*$') == true"),
  ];

  for (name, script) in tests {
      assert_eq!(test_script(script, None), Ok(true.into()), "{}", name);
  }
}
```

I'm getting a `Err(NoSuchKey("matches"))` for both `map` & `string` tests. Couldn't see anything mentioning regular expressions neither. So here's the questions, is this a conscious decision to _not support_ them? Looking around, I couldn't find a good candidate regex lib to start implementing that support actually... especially as in our use-case we're looking at targeting wasm... 

Have you considered that side of the spec? Any conclusion you came to already? 


Discussion / hints:
Definitely not intentional, it's been more a lack of demand than anything. I'll definitely get this on the priority list, or I would be happy to work through a PR with you if you'd like to take a stab at it as well. Otherwise I'll take a look later this week.
Great! I'm certainly willing to have a shot at this, as if I can make it work here, I don't have to support two different code path in my own on-going work (see [here](https://github.com/Kuadrant/wasm-shim/pull/41), tho I'm rewriting the entire support for typing leveraging CEL, as that's how many types are actually represented with [Envoy](https://www.envoyproxy.io/) anyways). 
I'll look at supporting this while keeping it all working in wasm too then tho. I'll start looking around for a way to support that and will report my findings here. If you have any leads, now or later, please drop them here too 🙏 
Thanks for the awesome work you've done on CEL for Rust already!
Looks like [regex](https://github.com/rust-lang/regex) would do the job... I'll start looking into leveraging it to add support for regex in the interpreter

Fix patch:
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..6ca5409 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::{Error, Regex};
 use std::cmp::Ordering;
 use std::convert::TryInto;
 use std::sync::Arc;
@@ -223,6 +224,23 @@ pub fn starts_with(This(this): This<Arc<String>>, prefix: Arc<String>) -> 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(
+    ftx: &FunctionContext,
+    This(this): This<Arc<String>>,
+    regex: Arc<String>,
+) -> Result<bool> {
+    match Regex::new(&regex) {
+        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
 /// 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..014fdbd 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 {
@@ -140,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;
@@ -199,6 +202,39 @@ 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_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![
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());
-
     }
 }


Test patch: