Skip to main content

dakera_client/
filter.rs

1/// Typed filter builder helpers for the Dakera metadata filter DSL.
2///
3/// All functions return `serde_json::Value` so they compose directly with
4/// `with_filter(...)` on any request builder.
5///
6/// # Examples
7///
8/// ```no_run
9/// use dakera_client::filter as F;
10/// use serde_json::json;
11///
12/// // Recall memories tagged for a specific person (CE-79 array operator)
13/// let f = json!({"tags": F::array_contains("entity:PERSON:alice")});
14///
15/// // Logical combinator
16/// let f = F::and([
17///     json!({"importance": F::gte(0.8)}),
18///     json!({"tags": F::array_contains("entity:PERSON:alice")}),
19/// ]);
20/// ```
21use serde_json::{json, Value};
22
23// ---------------------------------------------------------------------------
24// Comparison operators
25// ---------------------------------------------------------------------------
26
27/// `$eq` — equal to `value`.
28pub fn eq(value: impl Into<Value>) -> Value {
29    json!({"$eq": value.into()})
30}
31
32/// `$ne` — not equal to `value`.
33pub fn ne(value: impl Into<Value>) -> Value {
34    json!({"$ne": value.into()})
35}
36
37/// `$gt` — greater than `value`.
38pub fn gt(value: impl Into<Value>) -> Value {
39    json!({"$gt": value.into()})
40}
41
42/// `$gte` — greater than or equal to `value`.
43pub fn gte(value: impl Into<Value>) -> Value {
44    json!({"$gte": value.into()})
45}
46
47/// `$lt` — less than `value`.
48pub fn lt(value: impl Into<Value>) -> Value {
49    json!({"$lt": value.into()})
50}
51
52/// `$lte` — less than or equal to `value`.
53pub fn lte(value: impl Into<Value>) -> Value {
54    json!({"$lte": value.into()})
55}
56
57/// `$in` — field value is in the given list.
58pub fn in_<I, V>(values: I) -> Value
59where
60    I: IntoIterator<Item = V>,
61    V: Into<Value>,
62{
63    let arr: Vec<Value> = values.into_iter().map(Into::into).collect();
64    json!({"$in": arr})
65}
66
67/// `$nin` — field value is NOT in the given list.
68pub fn nin<I, V>(values: I) -> Value
69where
70    I: IntoIterator<Item = V>,
71    V: Into<Value>,
72{
73    let arr: Vec<Value> = values.into_iter().map(Into::into).collect();
74    json!({"$nin": arr})
75}
76
77/// `$exists` — field presence check.
78pub fn exists(present: bool) -> Value {
79    json!({"$exists": present})
80}
81
82// ---------------------------------------------------------------------------
83// String operators
84// ---------------------------------------------------------------------------
85
86/// `$contains` — case-sensitive substring match.
87pub fn contains(substr: &str) -> Value {
88    json!({"$contains": substr})
89}
90
91/// `$icontains` — case-insensitive substring match.
92pub fn icontains(substr: &str) -> Value {
93    json!({"$icontains": substr})
94}
95
96/// `$startsWith` — prefix match.
97pub fn starts_with(prefix: &str) -> Value {
98    json!({"$startsWith": prefix})
99}
100
101/// `$endsWith` — suffix match.
102pub fn ends_with(suffix: &str) -> Value {
103    json!({"$endsWith": suffix})
104}
105
106/// `$glob` — glob pattern match (supports `*` and `?` wildcards).
107pub fn glob(pattern: &str) -> Value {
108    json!({"$glob": pattern})
109}
110
111/// `$regex` — regular expression match.
112pub fn regex(pattern: &str) -> Value {
113    json!({"$regex": pattern})
114}
115
116// ---------------------------------------------------------------------------
117// Array operators (CE-79)
118// ---------------------------------------------------------------------------
119
120/// `$arrayContains` — the metadata array field contains `value`.
121///
122/// Primary use case: entity-scoped vector search via server-assigned tags
123/// (e.g. `entity:PERSON:alice`). Enables HNSW pre-filtering to a single
124/// entity's memories before semantic ranking.
125pub fn array_contains(value: impl Into<Value>) -> Value {
126    json!({"$arrayContains": value.into()})
127}
128
129/// `$arrayContainsAll` — the metadata array field contains ALL of `values`.
130pub fn array_contains_all<I, V>(values: I) -> Value
131where
132    I: IntoIterator<Item = V>,
133    V: Into<Value>,
134{
135    let arr: Vec<Value> = values.into_iter().map(Into::into).collect();
136    json!({"$arrayContainsAll": arr})
137}
138
139/// `$arrayContainsAny` — the metadata array field contains ANY of `values`.
140pub fn array_contains_any<I, V>(values: I) -> Value
141where
142    I: IntoIterator<Item = V>,
143    V: Into<Value>,
144{
145    let arr: Vec<Value> = values.into_iter().map(Into::into).collect();
146    json!({"$arrayContainsAny": arr})
147}
148
149// ---------------------------------------------------------------------------
150// Logical combinators
151// ---------------------------------------------------------------------------
152
153/// `$and` — all conditions must match.
154pub fn and<I>(conditions: I) -> Value
155where
156    I: IntoIterator<Item = Value>,
157{
158    let arr: Vec<Value> = conditions.into_iter().collect();
159    json!({"$and": arr})
160}
161
162/// `$or` — at least one condition must match.
163pub fn or<I>(conditions: I) -> Value
164where
165    I: IntoIterator<Item = Value>,
166{
167    let arr: Vec<Value> = conditions.into_iter().collect();
168    json!({"$or": arr})
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use serde_json::json;
175
176    #[test]
177    fn test_comparison_ops() {
178        assert_eq!(eq("hello"), json!({"$eq": "hello"}));
179        assert_eq!(gte(0.8_f64), json!({"$gte": 0.8}));
180        assert_eq!(lt(100_i64), json!({"$lt": 100}));
181    }
182
183    #[test]
184    fn test_string_ops() {
185        assert_eq!(contains("alice"), json!({"$contains": "alice"}));
186        assert_eq!(icontains("Alice"), json!({"$icontains": "Alice"}));
187        assert_eq!(starts_with("entity:"), json!({"$startsWith": "entity:"}));
188        assert_eq!(ends_with(":alice"), json!({"$endsWith": ":alice"}));
189        assert_eq!(glob("entity:*:alice"), json!({"$glob": "entity:*:alice"}));
190        assert_eq!(
191            regex("^entity:PERSON:"),
192            json!({"$regex": "^entity:PERSON:"})
193        );
194    }
195
196    #[test]
197    fn test_array_ops() {
198        assert_eq!(
199            array_contains("entity:PERSON:alice"),
200            json!({"$arrayContains": "entity:PERSON:alice"})
201        );
202        assert_eq!(
203            array_contains_all(["entity:PERSON:alice", "entity:PERSON:bob"]),
204            json!({"$arrayContainsAll": ["entity:PERSON:alice", "entity:PERSON:bob"]})
205        );
206        assert_eq!(
207            array_contains_any(["entity:PERSON:alice", "entity:PERSON:carol"]),
208            json!({"$arrayContainsAny": ["entity:PERSON:alice", "entity:PERSON:carol"]})
209        );
210    }
211
212    #[test]
213    fn test_logical_ops() {
214        let f = and([
215            json!({"importance": gte(0.8_f64)}),
216            json!({"tags": array_contains("entity:PERSON:alice")}),
217        ]);
218        assert_eq!(
219            f,
220            json!({"$and": [
221                {"importance": {"$gte": 0.8}},
222                {"tags": {"$arrayContains": "entity:PERSON:alice"}}
223            ]})
224        );
225    }
226
227    #[test]
228    fn test_in_nin() {
229        assert_eq!(in_(["a", "b", "c"]), json!({"$in": ["a", "b", "c"]}));
230        assert_eq!(nin([1_i64, 2_i64, 3_i64]), json!({"$nin": [1, 2, 3]}));
231    }
232
233    #[test]
234    fn test_exists() {
235        assert_eq!(exists(true), json!({"$exists": true}));
236        assert_eq!(exists(false), json!({"$exists": false}));
237    }
238}