Skip to main content

qubit_metadata/
filter.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! Provides [`MetadataFilter`] — composable filter expressions for
10//! metadata-based queries.
11//!
12//! A [`MetadataFilter`] can be used to select [`Metadata`] instances that
13//! satisfy a set of conditions.  Conditions can be combined with logical
14//! operators (`and`, `or`, `not`) to form arbitrarily complex predicates.
15//!
16//! # Examples
17//!
18//! ```rust
19//! use qubit_metadata::{Metadata, MetadataFilter};
20//!
21//! let mut meta = Metadata::new();
22//! meta.set("status", "active");
23//! meta.set("score", 42_i64);
24//!
25//! let filter = MetadataFilter::eq("status", "active")
26//!     .and(MetadataFilter::gte("score", 10_i64));
27//!
28//! assert!(filter.matches(&meta));
29//! ```
30
31use serde::{
32    Deserialize,
33    Serialize,
34};
35use std::cmp::Ordering;
36
37use serde_json::{
38    Number,
39    Value,
40};
41
42use crate::Metadata;
43
44/// A single comparison operator applied to one metadata key.
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub enum Condition {
47    /// Key equals value.
48    Eq {
49        /// The metadata key.
50        key: String,
51        /// The expected value.
52        value: Value,
53    },
54    /// Key does not equal value.
55    Ne {
56        /// The metadata key.
57        key: String,
58        /// The value to compare against.
59        value: Value,
60    },
61    /// Key is greater than value (numeric / string comparison).
62    Gt {
63        /// The metadata key.
64        key: String,
65        /// The lower bound (exclusive).
66        value: Value,
67    },
68    /// Key is greater than or equal to value.
69    Gte {
70        /// The metadata key.
71        key: String,
72        /// The lower bound (inclusive).
73        value: Value,
74    },
75    /// Key is less than value.
76    Lt {
77        /// The metadata key.
78        key: String,
79        /// The upper bound (exclusive).
80        value: Value,
81    },
82    /// Key is less than or equal to value.
83    Lte {
84        /// The metadata key.
85        key: String,
86        /// The upper bound (inclusive).
87        value: Value,
88    },
89    /// Key exists in the metadata (regardless of its value).
90    Exists {
91        /// The metadata key.
92        key: String,
93    },
94    /// Key does not exist in the metadata.
95    NotExists {
96        /// The metadata key.
97        key: String,
98    },
99    /// The stored value is one of the listed candidates.
100    In {
101        /// The metadata key.
102        key: String,
103        /// The set of acceptable values.
104        values: Vec<Value>,
105    },
106    /// The stored value is not any of the listed candidates.
107    NotIn {
108        /// The metadata key.
109        key: String,
110        /// The set of excluded values.
111        values: Vec<Value>,
112    },
113}
114
115impl Condition {
116    fn matches(&self, meta: &Metadata) -> bool {
117        match self {
118            Condition::Eq { key, value } => meta.get_raw(key) == Some(value),
119            Condition::Ne { key, value } => meta.get_raw(key) != Some(value),
120            Condition::Gt { key, value } => meta
121                .get_raw(key)
122                .is_some_and(|v| compare_values(v, value) == Some(Ordering::Greater)),
123            Condition::Gte { key, value } => meta.get_raw(key).is_some_and(|v| {
124                matches!(
125                    compare_values(v, value),
126                    Some(Ordering::Greater) | Some(Ordering::Equal)
127                )
128            }),
129            Condition::Lt { key, value } => meta
130                .get_raw(key)
131                .is_some_and(|v| compare_values(v, value) == Some(Ordering::Less)),
132            Condition::Lte { key, value } => meta.get_raw(key).is_some_and(|v| {
133                matches!(
134                    compare_values(v, value),
135                    Some(Ordering::Less) | Some(Ordering::Equal)
136                )
137            }),
138            Condition::Exists { key } => meta.contains_key(key),
139            Condition::NotExists { key } => !meta.contains_key(key),
140            Condition::In { key, values } => meta.get_raw(key).is_some_and(|v| values.contains(v)),
141            Condition::NotIn { key, values } => {
142                meta.get_raw(key).map_or(true, |v| !values.contains(v))
143            }
144        }
145    }
146}
147
148/// Compares two [`Value`]s where both are the same numeric or string variant.
149/// Returns `None` when the values are incomparable (different types).
150fn compare_values(a: &Value, b: &Value) -> Option<Ordering> {
151    match (a, b) {
152        (Value::Number(x), Value::Number(y)) => compare_numbers(x, y),
153        (Value::String(x), Value::String(y)) => x.partial_cmp(y),
154        _ => None,
155    }
156}
157
158const MAX_SAFE_INTEGER_F64_U64: u64 = 9_007_199_254_740_992; // 2^53
159const I64_MIN_F64: f64 = -9_223_372_036_854_775_808.0; // -2^63
160const I64_EXCLUSIVE_MAX_F64: f64 = 9_223_372_036_854_775_808.0; // 2^63
161const U64_EXCLUSIVE_MAX_F64: f64 = 18_446_744_073_709_551_616.0; // 2^64
162
163fn compare_numbers(a: &Number, b: &Number) -> Option<Ordering> {
164    if let (Some(xi), Some(yi)) = (a.as_i64(), b.as_i64()) {
165        return Some(xi.cmp(&yi));
166    }
167    if let (Some(xi), Some(yu)) = (a.as_i64(), b.as_u64()) {
168        return Some(compare_i64_u64(xi, yu));
169    }
170    if let (Some(xu), Some(yi)) = (a.as_u64(), b.as_i64()) {
171        return Some(compare_i64_u64(yi, xu).reverse());
172    }
173    if let (Some(xu), Some(yu)) = (a.as_u64(), b.as_u64()) {
174        return Some(xu.cmp(&yu));
175    }
176    if let (Some(xi), Some(yf)) = (a.as_i64(), b.as_f64()) {
177        return compare_i64_f64(xi, yf);
178    }
179    if let (Some(xf), Some(yi)) = (a.as_f64(), b.as_i64()) {
180        return compare_i64_f64(yi, xf).map(Ordering::reverse);
181    }
182    if let (Some(xu), Some(yf)) = (a.as_u64(), b.as_f64()) {
183        return compare_u64_f64(xu, yf);
184    }
185    if let (Some(xf), Some(yu)) = (a.as_f64(), b.as_u64()) {
186        return compare_u64_f64(yu, xf).map(Ordering::reverse);
187    }
188    if let (Some(xf), Some(yf)) = (a.as_f64(), b.as_f64()) {
189        return xf.partial_cmp(&yf);
190    }
191
192    // serde_json::Number always represents one of i64/u64/f64.
193    unreachable!("Number must be representable as i64/u64/f64")
194}
195
196#[inline]
197fn compare_i64_u64(x: i64, y: u64) -> Ordering {
198    if x < 0 {
199        Ordering::Less
200    } else {
201        (x as u64).cmp(&y)
202    }
203}
204
205#[inline]
206fn compare_i64_f64(x: i64, y: f64) -> Option<Ordering> {
207    if y.fract() == 0.0 && (I64_MIN_F64..I64_EXCLUSIVE_MAX_F64).contains(&y) {
208        // Integer-vs-integer path avoids precision loss for values > 2^53.
209        return Some(x.cmp(&(y as i64)));
210    }
211
212    if x.unsigned_abs() <= MAX_SAFE_INTEGER_F64_U64 {
213        return (x as f64).partial_cmp(&y);
214    }
215
216    None
217}
218
219#[inline]
220fn compare_u64_f64(x: u64, y: f64) -> Option<Ordering> {
221    if y < 0.0 {
222        return Some(Ordering::Greater);
223    }
224
225    if y.fract() == 0.0 && (0.0..U64_EXCLUSIVE_MAX_F64).contains(&y) {
226        // Integer-vs-integer path avoids precision loss for values > 2^53.
227        return Some(x.cmp(&(y as u64)));
228    }
229
230    None
231}
232
233/// A composable filter expression over [`Metadata`].
234///
235/// Filters can be built from primitive [`Condition`]s and combined with
236/// [`MetadataFilter::and`], [`MetadataFilter::or`], and [`MetadataFilter::not`].
237///
238/// # Examples
239///
240/// ```rust
241/// use qubit_metadata::{Metadata, MetadataFilter};
242///
243/// let mut meta = Metadata::new();
244/// meta.set("env", "prod");
245/// meta.set("version", 2_i64);
246///
247/// let f = MetadataFilter::eq("env", "prod")
248///     .and(MetadataFilter::gte("version", 1_i64));
249///
250/// assert!(f.matches(&meta));
251/// ```
252#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
253pub enum MetadataFilter {
254    /// A leaf condition.
255    Condition(Condition),
256    /// All child filters must match.
257    And(Vec<MetadataFilter>),
258    /// At least one child filter must match.
259    Or(Vec<MetadataFilter>),
260    /// The child filter must not match.
261    Not(Box<MetadataFilter>),
262}
263
264impl MetadataFilter {
265    // ── Leaf constructors ────────────────────────────────────────────────────
266
267    /// Creates an equality filter: `key == value`.
268    pub fn eq<T: Serialize>(key: impl Into<String>, value: T) -> Self {
269        Self::Condition(Condition::Eq {
270            key: key.into(),
271            value: serde_json::to_value(value)
272                .expect("MetadataFilter::eq: value must be serializable"),
273        })
274    }
275
276    /// Creates a not-equal filter: `key != value`.
277    pub fn ne<T: Serialize>(key: impl Into<String>, value: T) -> Self {
278        Self::Condition(Condition::Ne {
279            key: key.into(),
280            value: serde_json::to_value(value)
281                .expect("MetadataFilter::ne: value must be serializable"),
282        })
283    }
284
285    /// Creates a greater-than filter: `key > value`.
286    pub fn gt<T: Serialize>(key: impl Into<String>, value: T) -> Self {
287        Self::Condition(Condition::Gt {
288            key: key.into(),
289            value: serde_json::to_value(value)
290                .expect("MetadataFilter::gt: value must be serializable"),
291        })
292    }
293
294    /// Creates a greater-than-or-equal filter: `key >= value`.
295    pub fn gte<T: Serialize>(key: impl Into<String>, value: T) -> Self {
296        Self::Condition(Condition::Gte {
297            key: key.into(),
298            value: serde_json::to_value(value)
299                .expect("MetadataFilter::gte: value must be serializable"),
300        })
301    }
302
303    /// Creates a less-than filter: `key < value`.
304    pub fn lt<T: Serialize>(key: impl Into<String>, value: T) -> Self {
305        Self::Condition(Condition::Lt {
306            key: key.into(),
307            value: serde_json::to_value(value)
308                .expect("MetadataFilter::lt: value must be serializable"),
309        })
310    }
311
312    /// Creates a less-than-or-equal filter: `key <= value`.
313    pub fn lte<T: Serialize>(key: impl Into<String>, value: T) -> Self {
314        Self::Condition(Condition::Lte {
315            key: key.into(),
316            value: serde_json::to_value(value)
317                .expect("MetadataFilter::lte: value must be serializable"),
318        })
319    }
320
321    /// Creates an existence filter: the key must be present.
322    pub fn exists(key: impl Into<String>) -> Self {
323        Self::Condition(Condition::Exists { key: key.into() })
324    }
325
326    /// Creates a non-existence filter: the key must be absent.
327    pub fn not_exists(key: impl Into<String>) -> Self {
328        Self::Condition(Condition::NotExists { key: key.into() })
329    }
330
331    /// Creates an in-set filter: `key ∈ values`.
332    pub fn in_values<T, I>(key: impl Into<String>, values: I) -> Self
333    where
334        T: Serialize,
335        I: IntoIterator<Item = T>,
336    {
337        let values = values
338            .into_iter()
339            .map(|v| {
340                serde_json::to_value(v)
341                    .expect("MetadataFilter::in_values: each value must be serializable")
342            })
343            .collect();
344        Self::Condition(Condition::In {
345            key: key.into(),
346            values,
347        })
348    }
349
350    /// Creates a not-in-set filter: `key ∉ values`.
351    pub fn not_in_values<T, I>(key: impl Into<String>, values: I) -> Self
352    where
353        T: Serialize,
354        I: IntoIterator<Item = T>,
355    {
356        let values = values
357            .into_iter()
358            .map(|v| {
359                serde_json::to_value(v)
360                    .expect("MetadataFilter::not_in_values: each value must be serializable")
361            })
362            .collect();
363        Self::Condition(Condition::NotIn {
364            key: key.into(),
365            values,
366        })
367    }
368
369    // ── Logical combinators ──────────────────────────────────────────────────
370
371    /// Combines `self` and `other` with a logical AND.
372    ///
373    /// If `self` is already an `And` node the new filter is appended to its
374    /// children rather than creating a new nested node.
375    #[must_use]
376    pub fn and(self, other: MetadataFilter) -> Self {
377        match self {
378            MetadataFilter::And(mut children) => {
379                children.push(other);
380                MetadataFilter::And(children)
381            }
382            _ => MetadataFilter::And(vec![self, other]),
383        }
384    }
385
386    /// Combines `self` and `other` with a logical OR.
387    ///
388    /// If `self` is already an `Or` node the new filter is appended to its
389    /// children rather than creating a new nested node.
390    #[must_use]
391    pub fn or(self, other: MetadataFilter) -> Self {
392        match self {
393            MetadataFilter::Or(mut children) => {
394                children.push(other);
395                MetadataFilter::Or(children)
396            }
397            _ => MetadataFilter::Or(vec![self, other]),
398        }
399    }
400
401    /// Wraps `self` in a logical NOT.
402    #[allow(clippy::should_implement_trait)]
403    #[must_use]
404    pub fn not(self) -> Self {
405        !self
406    }
407
408    // ── Evaluation ───────────────────────────────────────────────────────────
409
410    /// Returns `true` if `meta` satisfies this filter.
411    pub fn matches(&self, meta: &Metadata) -> bool {
412        match self {
413            MetadataFilter::Condition(cond) => cond.matches(meta),
414            MetadataFilter::And(children) => children.iter().all(|f| f.matches(meta)),
415            MetadataFilter::Or(children) => children.iter().any(|f| f.matches(meta)),
416            MetadataFilter::Not(inner) => !inner.matches(meta),
417        }
418    }
419}
420
421impl std::ops::Not for MetadataFilter {
422    type Output = MetadataFilter;
423
424    fn not(self) -> Self::Output {
425        MetadataFilter::Not(Box::new(self))
426    }
427}