Skip to main content

qubit_metadata/
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::equal("status", "active")
26//!     .and(MetadataFilter::greater_equal("score", 10_i64));
27//!
28//! assert!(filter.matches(&meta));
29//! ```
30
31use serde::{
32    Deserialize,
33    Serialize,
34};
35
36use crate::{
37    Condition,
38    Metadata,
39};
40
41/// A composable filter expression over [`Metadata`].
42///
43/// Filters can be built from primitive [`Condition`]s and combined with
44/// [`MetadataFilter::and`], [`MetadataFilter::or`], and [`MetadataFilter::not`].
45///
46/// # Examples
47///
48/// ```rust
49/// use qubit_metadata::{Metadata, MetadataFilter};
50///
51/// let mut meta = Metadata::new();
52/// meta.set("env", "prod");
53/// meta.set("version", 2_i64);
54///
55/// let f = MetadataFilter::equal("env", "prod")
56///     .and(MetadataFilter::greater_equal("version", 1_i64));
57///
58/// assert!(f.matches(&meta));
59/// ```
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub enum MetadataFilter {
62    /// A leaf condition.
63    Condition(Condition),
64    /// All child filters must match.
65    And(Vec<MetadataFilter>),
66    /// At least one child filter must match.
67    Or(Vec<MetadataFilter>),
68    /// The child filter must not match.
69    Not(Box<MetadataFilter>),
70}
71
72impl MetadataFilter {
73    // ── Leaf constructors ────────────────────────────────────────────────────
74
75    /// Creates an equality filter: `key == value`.
76    #[inline]
77    pub fn equal<T: Serialize>(key: impl Into<String>, value: T) -> Self {
78        Self::Condition(Condition::Equal {
79            key: key.into(),
80            value: serde_json::to_value(value)
81                .expect("MetadataFilter::equal: value must be serializable"),
82        })
83    }
84
85    /// Creates a not-equal filter: `key != value`.
86    #[inline]
87    pub fn not_equal<T: Serialize>(key: impl Into<String>, value: T) -> Self {
88        Self::Condition(Condition::NotEqual {
89            key: key.into(),
90            value: serde_json::to_value(value)
91                .expect("MetadataFilter::not_equal: value must be serializable"),
92        })
93    }
94
95    /// Creates a greater-than filter: `key > value`.
96    #[inline]
97    pub fn greater<T: Serialize>(key: impl Into<String>, value: T) -> Self {
98        Self::Condition(Condition::Greater {
99            key: key.into(),
100            value: serde_json::to_value(value)
101                .expect("MetadataFilter::greater: value must be serializable"),
102        })
103    }
104
105    /// Creates a greater-than-or-equal filter: `key >= value`.
106    #[inline]
107    pub fn greater_equal<T: Serialize>(key: impl Into<String>, value: T) -> Self {
108        Self::Condition(Condition::GreaterEqual {
109            key: key.into(),
110            value: serde_json::to_value(value)
111                .expect("MetadataFilter::greater_equal: value must be serializable"),
112        })
113    }
114
115    /// Creates a less-than filter: `key < value`.
116    #[inline]
117    pub fn less<T: Serialize>(key: impl Into<String>, value: T) -> Self {
118        Self::Condition(Condition::Less {
119            key: key.into(),
120            value: serde_json::to_value(value)
121                .expect("MetadataFilter::less: value must be serializable"),
122        })
123    }
124
125    /// Creates a less-than-or-equal filter: `key <= value`.
126    #[inline]
127    pub fn less_equal<T: Serialize>(key: impl Into<String>, value: T) -> Self {
128        Self::Condition(Condition::LessEqual {
129            key: key.into(),
130            value: serde_json::to_value(value)
131                .expect("MetadataFilter::less_equal: value must be serializable"),
132        })
133    }
134
135    /// Creates an existence filter: the key must be present.
136    #[inline]
137    pub fn exists(key: impl Into<String>) -> Self {
138        Self::Condition(Condition::Exists { key: key.into() })
139    }
140
141    /// Creates a non-existence filter: the key must be absent.
142    #[inline]
143    pub fn not_exists(key: impl Into<String>) -> Self {
144        Self::Condition(Condition::NotExists { key: key.into() })
145    }
146
147    /// Creates an in-set filter: `key ∈ values`.
148    #[inline]
149    pub fn in_values<T, I>(key: impl Into<String>, values: I) -> Self
150    where
151        T: Serialize,
152        I: IntoIterator<Item = T>,
153    {
154        let values = values
155            .into_iter()
156            .map(|v| {
157                serde_json::to_value(v)
158                    .expect("MetadataFilter::in_values: each value must be serializable")
159            })
160            .collect();
161        Self::Condition(Condition::In {
162            key: key.into(),
163            values,
164        })
165    }
166
167    /// Creates a not-in-set filter: `key ∉ values`.
168    #[inline]
169    pub fn not_in_values<T, I>(key: impl Into<String>, values: I) -> Self
170    where
171        T: Serialize,
172        I: IntoIterator<Item = T>,
173    {
174        let values = values
175            .into_iter()
176            .map(|v| {
177                serde_json::to_value(v)
178                    .expect("MetadataFilter::not_in_values: each value must be serializable")
179            })
180            .collect();
181        Self::Condition(Condition::NotIn {
182            key: key.into(),
183            values,
184        })
185    }
186
187    // ── Logical combinators ──────────────────────────────────────────────────
188
189    /// Combines `self` and `other` with a logical AND.
190    ///
191    /// If `self` is already an `And` node the new filter is appended to its
192    /// children rather than creating a new nested node.
193    #[inline]
194    #[must_use]
195    pub fn and(self, other: MetadataFilter) -> Self {
196        match self {
197            MetadataFilter::And(mut children) => {
198                children.push(other);
199                MetadataFilter::And(children)
200            }
201            _ => MetadataFilter::And(vec![self, other]),
202        }
203    }
204
205    /// Combines `self` and `other` with a logical OR.
206    ///
207    /// If `self` is already an `Or` node the new filter is appended to its
208    /// children rather than creating a new nested node.
209    #[inline]
210    #[must_use]
211    pub fn or(self, other: MetadataFilter) -> Self {
212        match self {
213            MetadataFilter::Or(mut children) => {
214                children.push(other);
215                MetadataFilter::Or(children)
216            }
217            _ => MetadataFilter::Or(vec![self, other]),
218        }
219    }
220
221    /// Wraps `self` in a logical NOT.
222    #[allow(clippy::should_implement_trait)]
223    #[inline]
224    #[must_use]
225    pub fn not(self) -> Self {
226        !self
227    }
228
229    // ── Evaluation ───────────────────────────────────────────────────────────
230
231    /// Returns `true` if `meta` satisfies this filter.
232    #[inline]
233    pub fn matches(&self, meta: &Metadata) -> bool {
234        match self {
235            MetadataFilter::Condition(cond) => cond.matches(meta),
236            MetadataFilter::And(children) => children.iter().all(|f| f.matches(meta)),
237            MetadataFilter::Or(children) => children.iter().any(|f| f.matches(meta)),
238            MetadataFilter::Not(inner) => !inner.matches(meta),
239        }
240    }
241}
242
243impl std::ops::Not for MetadataFilter {
244    type Output = MetadataFilter;
245
246    #[inline]
247    fn not(self) -> Self::Output {
248        MetadataFilter::Not(Box::new(self))
249    }
250}