use common::{FilterCondition, FilterExpression, FilterValue};
use regex::Regex;
use serde_json::Value;
pub fn evaluate_filter(filter: &FilterExpression, metadata: Option<&Value>) -> bool {
match filter {
FilterExpression::And { conditions } => {
conditions.iter().all(|c| evaluate_filter(c, metadata))
}
FilterExpression::Or { conditions } => {
conditions.iter().any(|c| evaluate_filter(c, metadata))
}
FilterExpression::Field { field } => {
for (field_name, condition) in field {
if !evaluate_field_condition(field_name, condition, metadata) {
return false;
}
}
true
}
}
}
fn evaluate_field_condition(
field_name: &str,
condition: &FilterCondition,
metadata: Option<&Value>,
) -> bool {
let field_value = get_nested_field(metadata, field_name);
match condition {
FilterCondition::Exists(should_exist) => field_value.is_some() == *should_exist,
FilterCondition::Eq(filter_val) => {
field_value.is_some_and(|v| compare_values_eq(v, filter_val))
}
FilterCondition::Ne(filter_val) => {
field_value.is_none_or(|v| !compare_values_eq(v, filter_val))
}
FilterCondition::Gt(filter_val) => field_value.is_some_and(|v| {
compare_values_ord(v, filter_val) == Some(std::cmp::Ordering::Greater)
}),
FilterCondition::Gte(filter_val) => field_value.is_some_and(|v| {
matches!(
compare_values_ord(v, filter_val),
Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
)
}),
FilterCondition::Lt(filter_val) => field_value
.is_some_and(|v| compare_values_ord(v, filter_val) == Some(std::cmp::Ordering::Less)),
FilterCondition::Lte(filter_val) => field_value.is_some_and(|v| {
matches!(
compare_values_ord(v, filter_val),
Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
)
}),
FilterCondition::In(values) => {
field_value.is_some_and(|v| values.iter().any(|fv| compare_values_eq(v, fv)))
}
FilterCondition::NotIn(values) => {
field_value.is_none_or(|v| !values.iter().any(|fv| compare_values_eq(v, fv)))
}
FilterCondition::Contains(substring) => field_value.is_some_and(|v| {
if let Value::String(s) = v {
s.contains(substring.as_str())
} else {
false
}
}),
FilterCondition::IContains(substring) => field_value.is_some_and(|v| {
if let Value::String(s) = v {
s.to_lowercase().contains(&substring.to_lowercase())
} else {
false
}
}),
FilterCondition::StartsWith(prefix) => field_value.is_some_and(|v| {
if let Value::String(s) = v {
s.starts_with(prefix.as_str())
} else {
false
}
}),
FilterCondition::EndsWith(suffix) => field_value.is_some_and(|v| {
if let Value::String(s) = v {
s.ends_with(suffix.as_str())
} else {
false
}
}),
FilterCondition::Glob(pattern) => field_value.is_some_and(|v| {
if let Value::String(s) = v {
glob_match(pattern, s)
} else {
false
}
}),
FilterCondition::Regex(pattern) => field_value.is_some_and(|v| {
if let Value::String(s) = v {
Regex::new(pattern)
.map(|re| re.is_match(s))
.unwrap_or(false)
} else {
false
}
}),
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
let pattern_chars: Vec<char> = pattern.chars().collect();
let text_chars: Vec<char> = text.chars().collect();
glob_match_recursive(&pattern_chars, &text_chars, 0, 0)
}
fn glob_match_recursive(pattern: &[char], text: &[char], pi: usize, ti: usize) -> bool {
if pi >= pattern.len() && ti >= text.len() {
return true;
}
if pi >= pattern.len() {
return false;
}
let pc = pattern[pi];
match pc {
'*' => {
if glob_match_recursive(pattern, text, pi + 1, ti) {
return true;
}
if ti < text.len() && glob_match_recursive(pattern, text, pi, ti + 1) {
return true;
}
false
}
'?' => {
if ti < text.len() {
glob_match_recursive(pattern, text, pi + 1, ti + 1)
} else {
false
}
}
_ => {
if ti < text.len() && pc == text[ti] {
glob_match_recursive(pattern, text, pi + 1, ti + 1)
} else {
false
}
}
}
}
fn get_nested_field<'a>(metadata: Option<&'a Value>, field_path: &str) -> Option<&'a Value> {
let metadata = metadata?;
let parts: Vec<&str> = field_path.split('.').collect();
let mut current = metadata;
for part in parts {
match current {
Value::Object(map) => {
current = map.get(part)?;
}
_ => return None,
}
}
Some(current)
}
fn compare_values_eq(json_val: &Value, filter_val: &FilterValue) -> bool {
match (json_val, filter_val) {
(Value::String(s), FilterValue::String(fs)) => s == fs,
(Value::Number(n), FilterValue::Number(fn_)) => {
n.as_f64().is_some_and(|nf| (nf - fn_).abs() < f64::EPSILON)
}
(Value::Number(n), FilterValue::Integer(fi)) => n.as_i64() == Some(*fi),
(Value::Bool(b), FilterValue::Boolean(fb)) => b == fb,
(Value::String(s), FilterValue::StringArray(arr)) => arr.contains(s),
(Value::Number(n), FilterValue::NumberArray(arr)) => n
.as_f64()
.is_some_and(|nf| arr.iter().any(|&af| (nf - af).abs() < f64::EPSILON)),
_ => false,
}
}
fn compare_values_ord(json_val: &Value, filter_val: &FilterValue) -> Option<std::cmp::Ordering> {
match (json_val, filter_val) {
(Value::String(s), FilterValue::String(fs)) => Some(s.cmp(fs)),
(Value::Number(n), FilterValue::Number(fn_)) => {
n.as_f64().and_then(|nf| nf.partial_cmp(fn_))
}
(Value::Number(n), FilterValue::Integer(fi)) => {
n.as_f64().and_then(|nf| nf.partial_cmp(&(*fi as f64)))
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::collections::HashMap;
fn make_field_filter(field: &str, condition: FilterCondition) -> FilterExpression {
let mut map = HashMap::new();
map.insert(field.to_string(), condition);
FilterExpression::Field { field: map }
}
#[test]
fn test_eq_string() {
let metadata = json!({"category": "electronics"});
let filter = make_field_filter(
"category",
FilterCondition::Eq(FilterValue::String("electronics".to_string())),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter(
"category",
FilterCondition::Eq(FilterValue::String("books".to_string())),
);
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_eq_number() {
let metadata = json!({"price": 99.99});
let filter = make_field_filter("price", FilterCondition::Eq(FilterValue::Number(99.99)));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("price", FilterCondition::Eq(FilterValue::Number(100.0)));
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_ne() {
let metadata = json!({"status": "active"});
let filter = make_field_filter(
"status",
FilterCondition::Ne(FilterValue::String("inactive".to_string())),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter(
"status",
FilterCondition::Ne(FilterValue::String("active".to_string())),
);
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_gt_lt() {
let metadata = json!({"price": 50.0});
let filter = make_field_filter("price", FilterCondition::Gt(FilterValue::Number(40.0)));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("price", FilterCondition::Gt(FilterValue::Number(50.0)));
assert!(!evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("price", FilterCondition::Lt(FilterValue::Number(60.0)));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("price", FilterCondition::Lt(FilterValue::Number(50.0)));
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_gte_lte() {
let metadata = json!({"count": 10});
let filter = make_field_filter("count", FilterCondition::Gte(FilterValue::Integer(10)));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("count", FilterCondition::Gte(FilterValue::Integer(11)));
assert!(!evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("count", FilterCondition::Lte(FilterValue::Integer(10)));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("count", FilterCondition::Lte(FilterValue::Integer(9)));
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_in() {
let metadata = json!({"category": "electronics"});
let filter = make_field_filter(
"category",
FilterCondition::In(vec![
FilterValue::String("electronics".to_string()),
FilterValue::String("computers".to_string()),
]),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter(
"category",
FilterCondition::In(vec![
FilterValue::String("books".to_string()),
FilterValue::String("clothing".to_string()),
]),
);
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_not_in() {
let metadata = json!({"status": "active"});
let filter = make_field_filter(
"status",
FilterCondition::NotIn(vec![
FilterValue::String("deleted".to_string()),
FilterValue::String("archived".to_string()),
]),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter(
"status",
FilterCondition::NotIn(vec![
FilterValue::String("active".to_string()),
FilterValue::String("pending".to_string()),
]),
);
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_exists() {
let metadata = json!({"name": "test", "value": null});
let filter = make_field_filter("name", FilterCondition::Exists(true));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("missing", FilterCondition::Exists(true));
assert!(!evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("missing", FilterCondition::Exists(false));
assert!(evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_nested_field() {
let metadata = json!({
"user": {
"name": "Alice",
"profile": {
"age": 30
}
}
});
let filter = make_field_filter(
"user.name",
FilterCondition::Eq(FilterValue::String("Alice".to_string())),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter(
"user.profile.age",
FilterCondition::Gte(FilterValue::Integer(18)),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_and_combinator() {
let metadata = json!({"category": "electronics", "price": 50.0});
let filter = FilterExpression::And {
conditions: vec![
make_field_filter(
"category",
FilterCondition::Eq(FilterValue::String("electronics".to_string())),
),
make_field_filter("price", FilterCondition::Lt(FilterValue::Number(100.0))),
],
};
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = FilterExpression::And {
conditions: vec![
make_field_filter(
"category",
FilterCondition::Eq(FilterValue::String("electronics".to_string())),
),
make_field_filter("price", FilterCondition::Gt(FilterValue::Number(100.0))),
],
};
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_or_combinator() {
let metadata = json!({"category": "electronics", "price": 150.0});
let filter = FilterExpression::Or {
conditions: vec![
make_field_filter(
"category",
FilterCondition::Eq(FilterValue::String("books".to_string())),
),
make_field_filter("price", FilterCondition::Gt(FilterValue::Number(100.0))),
],
};
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = FilterExpression::Or {
conditions: vec![
make_field_filter(
"category",
FilterCondition::Eq(FilterValue::String("books".to_string())),
),
make_field_filter("price", FilterCondition::Lt(FilterValue::Number(100.0))),
],
};
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_no_metadata() {
let filter = make_field_filter(
"category",
FilterCondition::Eq(FilterValue::String("test".to_string())),
);
assert!(!evaluate_filter(&filter, None));
let filter = make_field_filter("anything", FilterCondition::Exists(false));
assert!(evaluate_filter(&filter, None));
}
#[test]
fn test_boolean() {
let metadata = json!({"active": true, "verified": false});
let filter = make_field_filter("active", FilterCondition::Eq(FilterValue::Boolean(true)));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter =
make_field_filter("verified", FilterCondition::Eq(FilterValue::Boolean(false)));
assert!(evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_contains() {
let metadata = json!({"description": "Hello World Example"});
let filter = make_field_filter(
"description",
FilterCondition::Contains("World".to_string()),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter(
"description",
FilterCondition::Contains("world".to_string()),
);
assert!(!evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter(
"description",
FilterCondition::Contains("NotFound".to_string()),
);
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_icontains() {
let metadata = json!({"description": "Hello World Example"});
let filter = make_field_filter(
"description",
FilterCondition::IContains("world".to_string()),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter(
"description",
FilterCondition::IContains("WORLD".to_string()),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter(
"description",
FilterCondition::IContains("notfound".to_string()),
);
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_starts_with() {
let metadata = json!({"filename": "document.pdf"});
let filter = make_field_filter(
"filename",
FilterCondition::StartsWith("document".to_string()),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("filename", FilterCondition::StartsWith("doc".to_string()));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("filename", FilterCondition::StartsWith("pdf".to_string()));
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_ends_with() {
let metadata = json!({"filename": "document.pdf"});
let filter = make_field_filter("filename", FilterCondition::EndsWith(".pdf".to_string()));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("filename", FilterCondition::EndsWith("pdf".to_string()));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("filename", FilterCondition::EndsWith(".txt".to_string()));
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_glob() {
let metadata = json!({"path": "src/main/java/App.java"});
let filter = make_field_filter("path", FilterCondition::Glob("*.java".to_string()));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("path", FilterCondition::Glob("src/*".to_string()));
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("path", FilterCondition::Glob("*App*".to_string()));
assert!(evaluate_filter(&filter, Some(&metadata)));
let metadata2 = json!({"code": "A1B2"});
let filter = make_field_filter("code", FilterCondition::Glob("A?B?".to_string()));
assert!(evaluate_filter(&filter, Some(&metadata2)));
let filter = make_field_filter("code", FilterCondition::Glob("A?B".to_string()));
assert!(!evaluate_filter(&filter, Some(&metadata2)));
let filter = make_field_filter("path", FilterCondition::Glob("*.rs".to_string()));
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_regex() {
let metadata = json!({"email": "user@example.com"});
let filter = make_field_filter(
"email",
FilterCondition::Regex(r"^[\w.]+@[\w.]+\.\w+$".to_string()),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter(
"email",
FilterCondition::Regex(r"@example\.com$".to_string()),
);
assert!(evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("email", FilterCondition::Regex(r"^admin@".to_string()));
assert!(!evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("email", FilterCondition::Regex(r"[invalid".to_string()));
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
#[test]
fn test_string_operators_on_non_string() {
let metadata = json!({"count": 42});
let filter = make_field_filter("count", FilterCondition::Contains("4".to_string()));
assert!(!evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("count", FilterCondition::StartsWith("4".to_string()));
assert!(!evaluate_filter(&filter, Some(&metadata)));
let filter = make_field_filter("count", FilterCondition::Glob("*".to_string()));
assert!(!evaluate_filter(&filter, Some(&metadata)));
}
}