From fe7ba6996d42f10ed7b2fd91b7735c8283f7a806 Mon Sep 17 00:00:00 2001 From: stefan-gorules <127550877+stefan-gorules@users.noreply.github.com> Date: Sun, 2 Jul 2023 19:42:58 +0200 Subject: [PATCH] feat: add regular expression functions; (#52) * feat: add regular expression functions; * add cargo.toml regex * fix cargo fmt --- Cargo.toml | 1 + core/expression/Cargo.toml | 1 + core/expression/src/compiler.rs | 10 ++++ core/expression/src/opcodes.rs | 2 + .../src/parser/standard/constants.rs | 3 ++ core/expression/src/vm.rs | 51 +++++++++++++++++++ core/expression/tests/isolate.rs | 21 ++++++++ 7 files changed, 89 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 6bc7823a..e2e872c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ fastrand = "1.9.0" humantime = "2.1.0" once_cell = "1.17.2" phf = "0.11.1" +regex = "1.8.4" serde_json = "1.0.96" hashbrown = "0.13.2" rust_decimal = "1.29.1" diff --git a/core/expression/Cargo.toml b/core/expression/Cargo.toml index daac5cf0..be29d33b 100644 --- a/core/expression/Cargo.toml +++ b/core/expression/Cargo.toml @@ -17,6 +17,7 @@ humantime = { workspace = true } fastrand = { workspace = true } once_cell = { workspace = true } phf = { workspace = true, features = ["macros"] } +regex = { workspace = true } serde_json = { workspace = true, features = ["arbitrary_precision"] } thiserror = { workspace = true } rust_decimal = { workspace = true, features = ["maths-nopanic"] } diff --git a/core/expression/src/compiler.rs b/core/expression/src/compiler.rs index ffef6669..d11035b0 100644 --- a/core/expression/src/compiler.rs +++ b/core/expression/src/compiler.rs @@ -321,6 +321,16 @@ impl<'a> Compiler<'a> { self.compile_argument(name, arguments, 1)?; Ok(self.emit(Opcode::Contains)) } + "matches" => { + self.compile_argument(name, arguments, 0)?; + self.compile_argument(name, arguments, 1)?; + Ok(self.emit(Opcode::Matches)) + } + "extract" => { + self.compile_argument(name, arguments, 0)?; + self.compile_argument(name, arguments, 1)?; + Ok(self.emit(Opcode::Extract)) + } "flatten" => { self.compile_argument(name, arguments, 0)?; Ok(self.emit(Opcode::Flatten)) diff --git a/core/expression/src/opcodes.rs b/core/expression/src/opcodes.rs index d357dc58..cc9b56da 100644 --- a/core/expression/src/opcodes.rs +++ b/core/expression/src/opcodes.rs @@ -108,6 +108,8 @@ pub enum Opcode<'a> { Lowercase, StartsWith, EndsWith, + Matches, + Extract, Slice, Array, Len, diff --git a/core/expression/src/parser/standard/constants.rs b/core/expression/src/parser/standard/constants.rs index 99da2210..97be3cbb 100644 --- a/core/expression/src/parser/standard/constants.rs +++ b/core/expression/src/parser/standard/constants.rs @@ -44,6 +44,9 @@ pub(crate) static BUILT_INS: Lazy> = "startsWith" => BuiltIn { arity: Arity::Dual }, "endsWith" => BuiltIn { arity: Arity::Dual }, "contains" => BuiltIn { arity: Arity::Dual }, + "matches" => BuiltIn { arity: Arity::Dual }, + "extract" => BuiltIn { arity: Arity::Dual }, + "all" => BuiltIn { arity: Arity::Closure }, "some" => BuiltIn { arity: Arity::Closure }, "none" => BuiltIn { arity: Arity::Closure }, diff --git a/core/expression/src/vm.rs b/core/expression/src/vm.rs index c99c500c..10d8c3bd 100644 --- a/core/expression/src/vm.rs +++ b/core/expression/src/vm.rs @@ -1,6 +1,7 @@ use bumpalo::Bump; use chrono::NaiveDateTime; use chrono::{Datelike, Timelike}; +use regex::Regex; use rust_decimal::prelude::ToPrimitive; use rust_decimal::{Decimal, MathematicalOps}; use thiserror::Error; @@ -833,6 +834,56 @@ impl<'a> VM<'a> { } } } + Opcode::Matches => { + let b = self.pop()?; + let a = self.pop()?; + + let (String(a), String(b)) = (a, b) else { + return Err(OpcodeErr { + opcode: "Matches".into(), + message: "Unsupported type".into(), + }) + }; + + let regex = Regex::new(b).map_err(|_| OpcodeErr { + opcode: "Matches".into(), + message: "Invalid regular expression".into(), + })?; + + self.push(Bool(regex.is_match(a))); + } + Opcode::Extract => { + let b = self.pop()?; + let a = self.pop()?; + + let (String(a), String(b)) = (a, b) else { + return Err(OpcodeErr { + opcode: "Matches".into(), + message: "Unsupported type".into(), + }) + }; + + let regex = Regex::new(b).map_err(|_| OpcodeErr { + opcode: "Matches".into(), + message: "Invalid regular expression".into(), + })?; + + let captures = regex + .captures(a) + .map(|capture| { + capture + .iter() + .map(|c| c.map(|c| c.as_str())) + .filter_map(|c| c) + .map(|s| { + self.bump.alloc(String(self.bump.alloc_str(s))) as &Variable + }) + .collect::>() + }) + .unwrap_or_default(); + + self.push(Array(self.bump.alloc_slice_copy(captures.as_slice()))); + } Opcode::DateManipulation(operation) => { let timestamp = self.pop()?; diff --git a/core/expression/tests/isolate.rs b/core/expression/tests/isolate.rs index bf39d910..d6287fd3 100644 --- a/core/expression/tests/isolate.rs +++ b/core/expression/tests/isolate.rs @@ -486,6 +486,27 @@ fn isolate_standard_test() { }, ]), }, + TestEnv { + env: json!({}), + cases: Vec::from([ + TestCase { + expr: r#"extract("2022-02-01", "(\d{4})-(\d{2})-(\d{2})")"#, + result: json!(["2022-02-01", "2022", "02", "01"]), + }, + TestCase { + expr: r#"all(["babble", "bebble", "bibble", "bobble", "bubble"], matches(#, "b[aeiou]bble"))"#, + result: json!(true), + }, + TestCase { + expr: r#"none(["babble", "bebble", "bibble", "bobble", "bubble"], matches(#, "b[aeiou]bblo"))"#, + result: json!(true), + }, + TestCase { + expr: r#"extract("foo.bar", "(\w+)\.(\w+)")"#, + result: json!(["foo.bar", "foo", "bar"]), + }, + ]), + }, ]); let isolate = Isolate::default();