use anyhow::Result;
use regex::Regex;
use serde_json::Value;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::assert::engine::AssertionResult;
use crate::plugins::{
ArgTypeInfo, Plugin, PluginContext, PluginPurity, PluginResult, PluginSignature, TypeInfo,
};
thread_local! {
static REGEX_CACHE: RefCell<HashMap<String, std::result::Result<Rc<Regex>, String>>> =
RefCell::new(HashMap::new());
}
fn cached_regex(pattern: &str) -> std::result::Result<Rc<Regex>, String> {
if let Some(cached) = REGEX_CACHE.with(|cache| cache.borrow().get(pattern).cloned()) {
return cached;
}
let compiled = Regex::new(pattern)
.map(Rc::new)
.map_err(|err| err.to_string());
REGEX_CACHE.with(|cache| {
cache
.borrow_mut()
.insert(pattern.to_string(), compiled.clone());
});
compiled
}
#[derive(Debug, Clone, Default)]
pub struct RegexPlugin;
impl Plugin for RegexPlugin {
fn name(&self) -> &'static str {
"regex"
}
fn description(&self) -> &'static str {
"Validate field matches regex pattern"
}
fn signature(&self) -> PluginSignature {
PluginSignature {
return_type: TypeInfo::Bool,
arg_types: &[
ArgTypeInfo {
expected: TypeInfo::String,
required: true,
default: None,
},
ArgTypeInfo {
expected: TypeInfo::String,
required: true,
default: None,
},
],
purity: PluginPurity::Pure,
deterministic: true,
idempotent: true,
safe_for_rewrite: true,
arg_names: &["value", "pattern"],
}
}
fn execute(&self, args: &[Value], _context: &PluginContext) -> Result<PluginResult> {
if args.len() != 2 {
return Ok(PluginResult::Assertion(AssertionResult::fail(
"@regex requires 2 arguments: field and pattern",
)));
}
let field_value = match &args[0] {
Value::String(s) => s,
Value::Number(n) => {
return Ok(PluginResult::Assertion(AssertionResult::fail(format!(
"@regex expects string field, got number: {}",
n
))));
}
_ => {
return Ok(PluginResult::Assertion(AssertionResult::fail(
"@regex first argument must be a string",
)));
}
};
let pattern = match &args[1] {
Value::String(s) => s,
_ => {
return Ok(PluginResult::Assertion(AssertionResult::fail(
"@regex second argument (pattern) must be a string",
)));
}
};
match cached_regex(pattern) {
Ok(re) => {
if re.is_match(field_value) {
Ok(PluginResult::Assertion(AssertionResult::Pass))
} else {
Ok(PluginResult::Assertion(AssertionResult::fail(format!(
"Value '{}' does not match pattern '{}'",
field_value, pattern
))))
}
}
Err(e) => Ok(PluginResult::Assertion(AssertionResult::fail(format!(
"Invalid regex pattern '{}': {}",
pattern, e
)))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_context() -> PluginContext<'static> {
PluginContext::new(&Value::Null)
}
#[test]
fn test_regex_plugin_valid_email() {
let plugin = RegexPlugin;
let result = plugin
.execute(
&[
Value::String("test@example.com".to_string()),
Value::String(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$".to_string()),
],
&create_context(),
)
.unwrap();
assert!(matches!(
result,
PluginResult::Assertion(AssertionResult::Pass)
));
}
#[test]
fn test_regex_plugin_invalid_email() {
let plugin = RegexPlugin;
let result = plugin
.execute(
&[
Value::String("invalid-email".to_string()),
Value::String(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$".to_string()),
],
&create_context(),
)
.unwrap();
assert!(matches!(
result,
PluginResult::Assertion(AssertionResult::Fail { .. })
));
}
#[test]
fn test_regex_plugin_uuid() {
let plugin = RegexPlugin;
let result = plugin
.execute(
&[
Value::String("550e8400-e29b-41d4-a716-446655440000".to_string()),
Value::String(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
.to_string(),
),
],
&create_context(),
)
.unwrap();
assert!(matches!(
result,
PluginResult::Assertion(AssertionResult::Pass)
));
}
#[test]
fn test_regex_plugin_invalid_pattern() {
let plugin = RegexPlugin;
let result = plugin
.execute(
&[
Value::String("test".to_string()),
Value::String(r"[invalid(regex".to_string()),
],
&create_context(),
)
.unwrap();
assert!(matches!(
result,
PluginResult::Assertion(AssertionResult::Fail { .. })
));
}
#[test]
fn test_regex_plugin_wrong_arg_count() {
let plugin = RegexPlugin;
let result = plugin
.execute(&[Value::String("test".to_string())], &create_context())
.unwrap();
assert!(matches!(
result,
PluginResult::Assertion(AssertionResult::Fail { .. })
));
}
#[test]
fn test_regex_plugin_name() {
let plugin = RegexPlugin;
assert_eq!(plugin.name(), "regex");
}
#[test]
fn test_regex_plugin_description() {
let plugin = RegexPlugin;
assert!(plugin.description().contains("regex"));
}
}