use crate::types::MetadataValue;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum FilterOp {
Eq(MetadataValue),
Ne(MetadataValue),
Gt(MetadataValue),
Gte(MetadataValue),
Lt(MetadataValue),
Lte(MetadataValue),
In(Vec<MetadataValue>),
Nin(Vec<MetadataValue>),
Exists(bool),
Contains(String),
StartsWith(String),
EndsWith(String),
Regex(String),
}
impl FilterOp {
pub fn evaluate(&self, value: Option<&MetadataValue>) -> bool {
match self {
FilterOp::Exists(should_exist) => value.is_some() == *should_exist,
FilterOp::Eq(expected) => value.is_some_and(|v| values_equal(v, expected)),
FilterOp::Ne(expected) => value.is_none_or(|v| !values_equal(v, expected)),
FilterOp::Gt(threshold) => value
.is_some_and(|v| compare_values(v, threshold) == Some(std::cmp::Ordering::Greater)),
FilterOp::Gte(threshold) => value.is_some_and(|v| {
matches!(
compare_values(v, threshold),
Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
)
}),
FilterOp::Lt(threshold) => value
.is_some_and(|v| compare_values(v, threshold) == Some(std::cmp::Ordering::Less)),
FilterOp::Lte(threshold) => value.is_some_and(|v| {
matches!(
compare_values(v, threshold),
Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
)
}),
FilterOp::In(list) => {
value.is_some_and(|v| list.iter().any(|item| values_equal(v, item)))
}
FilterOp::Nin(list) => {
value.is_none_or(|v| !list.iter().any(|item| values_equal(v, item)))
}
FilterOp::Contains(substr) => match value {
Some(MetadataValue::String(s)) => s.to_lowercase().contains(&substr.to_lowercase()),
_ => false,
},
FilterOp::StartsWith(prefix) => match value {
Some(MetadataValue::String(s)) => {
s.to_lowercase().starts_with(&prefix.to_lowercase())
}
_ => false,
},
FilterOp::EndsWith(suffix) => match value {
Some(MetadataValue::String(s)) => {
s.to_lowercase().ends_with(&suffix.to_lowercase())
}
_ => false,
},
FilterOp::Regex(pattern) => match value {
Some(MetadataValue::String(s)) => regex_lite::Regex::new(pattern)
.map(|re| re.is_match(s))
.unwrap_or(false),
_ => false,
},
}
}
}
fn values_equal(a: &MetadataValue, b: &MetadataValue) -> bool {
match (a, b) {
(MetadataValue::String(s1), MetadataValue::String(s2)) => s1 == s2,
(MetadataValue::Int(i1), MetadataValue::Int(i2)) => i1 == i2,
(MetadataValue::Float(f1), MetadataValue::Float(f2)) => (f1 - f2).abs() < f64::EPSILON,
(MetadataValue::Bool(b1), MetadataValue::Bool(b2)) => b1 == b2,
(MetadataValue::Int(i), MetadataValue::Float(f)) => (*i as f64 - f).abs() < f64::EPSILON,
(MetadataValue::Float(f), MetadataValue::Int(i)) => (f - *i as f64).abs() < f64::EPSILON,
_ => false,
}
}
fn compare_values(a: &MetadataValue, b: &MetadataValue) -> Option<std::cmp::Ordering> {
match (a, b) {
(MetadataValue::Int(i1), MetadataValue::Int(i2)) => Some(i1.cmp(i2)),
(MetadataValue::Float(f1), MetadataValue::Float(f2)) => f1.partial_cmp(f2),
(MetadataValue::Int(i), MetadataValue::Float(f)) => (*i as f64).partial_cmp(f),
(MetadataValue::Float(f), MetadataValue::Int(i)) => f.partial_cmp(&(*i as f64)),
(MetadataValue::String(s1), MetadataValue::String(s2)) => Some(s1.cmp(s2)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_eq_string() {
let op = FilterOp::Eq(MetadataValue::String("test".into()));
assert!(op.evaluate(Some(&MetadataValue::String("test".into()))));
assert!(!op.evaluate(Some(&MetadataValue::String("other".into()))));
assert!(!op.evaluate(None));
}
#[test]
fn test_eq_int() {
let op = FilterOp::Eq(MetadataValue::Int(42));
assert!(op.evaluate(Some(&MetadataValue::Int(42))));
assert!(!op.evaluate(Some(&MetadataValue::Int(41))));
}
#[test]
fn test_numeric_comparison() {
let op = FilterOp::Gt(MetadataValue::Int(10));
assert!(op.evaluate(Some(&MetadataValue::Int(15))));
assert!(!op.evaluate(Some(&MetadataValue::Int(10))));
assert!(!op.evaluate(Some(&MetadataValue::Int(5))));
let op = FilterOp::Gte(MetadataValue::Float(0.5));
assert!(op.evaluate(Some(&MetadataValue::Float(0.5))));
assert!(op.evaluate(Some(&MetadataValue::Float(0.6))));
assert!(!op.evaluate(Some(&MetadataValue::Float(0.4))));
}
#[test]
fn test_cross_type_comparison() {
let op = FilterOp::Gt(MetadataValue::Int(10));
assert!(op.evaluate(Some(&MetadataValue::Float(10.5))));
assert!(!op.evaluate(Some(&MetadataValue::Float(9.5))));
}
#[test]
fn test_in_operator() {
let op = FilterOp::In(vec![
MetadataValue::String("a".into()),
MetadataValue::String("b".into()),
MetadataValue::String("c".into()),
]);
assert!(op.evaluate(Some(&MetadataValue::String("b".into()))));
assert!(!op.evaluate(Some(&MetadataValue::String("d".into()))));
}
#[test]
fn test_exists() {
let op = FilterOp::Exists(true);
assert!(op.evaluate(Some(&MetadataValue::String("any".into()))));
assert!(!op.evaluate(None));
let op = FilterOp::Exists(false);
assert!(!op.evaluate(Some(&MetadataValue::String("any".into()))));
assert!(op.evaluate(None));
}
#[test]
fn test_contains() {
let op = FilterOp::Contains("rust".into());
assert!(op.evaluate(Some(&MetadataValue::String(
"Learning Rust programming".into()
))));
assert!(op.evaluate(Some(&MetadataValue::String("RUST is great".into())))); assert!(!op.evaluate(Some(&MetadataValue::String("Python is cool".into()))));
}
#[test]
fn test_starts_with() {
let op = FilterOp::StartsWith("hello".into());
assert!(op.evaluate(Some(&MetadataValue::String("Hello World".into()))));
assert!(!op.evaluate(Some(&MetadataValue::String("World Hello".into()))));
}
#[test]
fn test_ends_with() {
let op = FilterOp::EndsWith(".rs".into());
assert!(op.evaluate(Some(&MetadataValue::String("main.rs".into()))));
assert!(!op.evaluate(Some(&MetadataValue::String("main.py".into()))));
}
}