use crate::engine::interfaces::{
GenericMiddleware, Middleware, MiddlewareOutput, ParamDef, ParamType, Plugin, ResolvedInputs,
};
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use fancy_log::{LogLevel, log};
use serde_json::Value;
use std::{any::Any, borrow::Cow};
pub struct CommonMatchPlugin;
impl Plugin for CommonMatchPlugin {
fn name(&self) -> &'static str {
"internal.common.match"
}
fn params(&self) -> Vec<ParamDef> {
vec![
ParamDef {
name: "left".into(),
required: true,
param_type: ParamType::String,
},
ParamDef {
name: "right".into(),
required: true,
param_type: ParamType::String,
},
ParamDef {
name: "operator".into(),
required: false,
param_type: ParamType::String,
},
]
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_middleware(&self) -> Option<&dyn Middleware> {
Some(self)
}
fn as_generic_middleware(&self) -> Option<&dyn GenericMiddleware> {
Some(self)
}
}
#[async_trait]
impl GenericMiddleware for CommonMatchPlugin {
fn output(&self) -> Vec<Cow<'static, str>> {
vec!["true".into(), "false".into()]
}
async fn execute(&self, inputs: ResolvedInputs) -> Result<MiddlewareOutput> {
let left = inputs
.get("left")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("Input 'left' missing or not a string"))?;
let right = inputs
.get("right")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("Input 'right' missing or not a string"))?;
let operator = inputs
.get("operator")
.and_then(Value::as_str)
.unwrap_or("==")
.to_lowercase();
log(
LogLevel::Debug,
&format!("⚙ Match Plugin: Comparing Left='{left}' with Right='{right}' (Op: '{operator}')"),
);
let result = match operator.as_str() {
"==" | "eq" | "equal" | "equals" => left == right,
"!=" | "ne" | "notequal" | "not_equal" => left != right,
"contains" | "contain" => left.contains(right),
"startswith" | "starts_with" => left.starts_with(right),
"endswith" | "ends_with" => left.ends_with(right),
"regex" | "re" | "match" => match regex::Regex::new(right) {
Ok(re) => re.is_match(left),
Err(e) => {
log(
LogLevel::Error,
&format!("✗ Invalid regex pattern '{right}': {e}"),
);
false
}
},
_ => false,
};
Ok(MiddlewareOutput {
branch: if result {
"true".into()
} else {
"false".into()
},
store: None,
})
}
}
#[async_trait]
impl Middleware for CommonMatchPlugin {
fn output(&self) -> Vec<Cow<'static, str>> {
<Self as GenericMiddleware>::output(self)
}
async fn execute(&self, inputs: ResolvedInputs) -> Result<MiddlewareOutput> {
<Self as GenericMiddleware>::execute(self, inputs).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[tokio::test]
async fn test_string_matching_operators() {
let plugin = CommonMatchPlugin;
let cases = vec![
("hello", "hello", "eq", "true"),
("hello", "world", "eq", "false"),
("hello", "HELLO", "eq", "false"),
("hello", "world", "ne", "true"),
("hello", "hello", "ne", "false"),
("hello world", "world", "contains", "true"),
("hello world", "rust", "contains", "false"),
("https://vane.com", "https://", "startswith", "true"),
("https://vane.com", "http://", "startswith", "false"),
("https://vane.com", "vane", "startswith", "false"),
("image.png", ".png", "endswith", "true"),
("image.png", ".jpg", "endswith", "false"),
("vane-123", r"^vane-\d+$", "regex", "true"),
("vane-abc", r"^vane-\d+$", "regex", "false"),
("test", "[invalid", "regex", "false"), ];
for (left, right, op, expected) in cases {
let mut inputs = HashMap::new();
inputs.insert("left".to_string(), Value::String(left.to_string()));
inputs.insert("right".to_string(), Value::String(right.to_string()));
inputs.insert("operator".to_string(), Value::String(op.to_string()));
let out = GenericMiddleware::execute(&plugin, inputs).await.unwrap();
assert_eq!(
out.branch, expected,
"Failed match: '{}' {} '{}' should be {}",
left, op, right, expected
);
}
}
#[tokio::test]
async fn test_default_operator() {
let plugin = CommonMatchPlugin;
let mut inputs = HashMap::new();
inputs.insert("left".to_string(), Value::String("a".to_string()));
inputs.insert("right".to_string(), Value::String("a".to_string()));
let out = GenericMiddleware::execute(&plugin, inputs).await.unwrap();
assert_eq!(out.branch, "true");
}
}