Skip to main content

agentdb/
filter.rs

1use serde_json::Value;
2
3/// Evaluate a metadata filter against a JSON document.
4///
5/// Supports exact match, comparison, and logical operators:
6/// `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$exists`,
7/// `$contains` (substring match), `$regex` (regex pattern match),
8/// `$and`, `$or`, `$not`.
9///
10/// Field paths support dot notation: `{ "user.name": { "$eq": "alice" } }`.
11pub fn matches(metadata: &Value, filter: &Value) -> bool {
12    let filter_obj = match filter {
13        Value::Object(f) => f,
14        _ => return false,
15    };
16    for (key, condition) in filter_obj {
17        match key.as_str() {
18            "$and" => {
19                if let Value::Array(clauses) = condition {
20                    if !clauses.iter().all(|c| matches(metadata, c)) {
21                        return false;
22                    }
23                } else {
24                    return false;
25                }
26            }
27            "$or" => {
28                if let Value::Array(clauses) = condition {
29                    if !clauses.iter().any(|c| matches(metadata, c)) {
30                        return false;
31                    }
32                } else {
33                    return false;
34                }
35            }
36            "$not" => {
37                if matches(metadata, condition) {
38                    return false;
39                }
40            }
41            field => {
42                let field_value = get_nested(metadata, field);
43                match condition {
44                    Value::Object(ops) => {
45                        for (op, operand) in ops {
46                            if !apply_op(op, field_value, operand) {
47                                return false;
48                            }
49                        }
50                    }
51                    expected => {
52                        if field_value != Some(expected) {
53                            return false;
54                        }
55                    }
56                }
57            }
58        }
59    }
60    true
61}
62
63/// Resolve a possibly dot-separated field path into the nested JSON value.
64fn get_nested<'a>(val: &'a Value, path: &str) -> Option<&'a Value> {
65    let mut current = val;
66    for segment in path.split('.') {
67        current = current.get(segment)?;
68    }
69    Some(current)
70}
71
72fn apply_op(op: &str, field: Option<&Value>, operand: &Value) -> bool {
73    match op {
74        "$eq" => field == Some(operand),
75        "$ne" => field != Some(operand),
76        "$exists" => field.is_some() == operand.as_bool().unwrap_or(true),
77        "$gt" => cmp_num(field, operand, |a, b| a > b),
78        "$gte" => cmp_num(field, operand, |a, b| a >= b),
79        "$lt" => cmp_num(field, operand, |a, b| a < b),
80        "$lte" => cmp_num(field, operand, |a, b| a <= b),
81        "$in" => match (field, operand) {
82            (Some(v), Value::Array(arr)) => arr.contains(v),
83            _ => false,
84        },
85        "$nin" => match (field, operand) {
86            (Some(v), Value::Array(arr)) => !arr.contains(v),
87            _ => true,
88        },
89        // Substring containment check (literal, not a regex pattern).
90        "$contains" => match (field, operand.as_str()) {
91            (Some(Value::String(s)), Some(pattern)) => s.contains(pattern),
92            _ => false,
93        },
94        // Regex pattern match. Invalid patterns never match (returns false).
95        "$regex" => match (field, operand.as_str()) {
96            (Some(Value::String(s)), Some(pattern)) => regex::Regex::new(pattern)
97                .map(|re| re.is_match(s))
98                .unwrap_or(false),
99            _ => false,
100        },
101        _ => false,
102    }
103}
104
105fn cmp_num<F>(field: Option<&Value>, operand: &Value, cmp: F) -> bool
106where
107    F: Fn(f64, f64) -> bool,
108{
109    match (field, operand) {
110        (Some(Value::Number(a)), Value::Number(b)) => match (a.as_f64(), b.as_f64()) {
111            (Some(av), Some(bv)) => cmp(av, bv),
112            _ => false,
113        },
114        _ => false,
115    }
116}