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