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    pub fn equal<T: Serialize>(key: impl Into<String>, value: T) -> Self {
77        Self::Condition(Condition::Equal {
78            key: key.into(),
79            value: serde_json::to_value(value)
80                .expect("MetadataFilter::equal: value must be serializable"),
81        })
82    }
83
84    /// Creates a not-equal filter: `key != value`.
85    pub fn not_equal<T: Serialize>(key: impl Into<String>, value: T) -> Self {
86        Self::Condition(Condition::NotEqual {
87            key: key.into(),
88            value: serde_json::to_value(value)
89                .expect("MetadataFilter::not_equal: value must be serializable"),
90        })
91    }
92
93    /// Creates a greater-than filter: `key > value`.
94    pub fn greater<T: Serialize>(key: impl Into<String>, value: T) -> Self {
95        Self::Condition(Condition::Greater {
96            key: key.into(),
97            value: serde_json::to_value(value)
98                .expect("MetadataFilter::greater: value must be serializable"),
99        })
100    }
101
102    /// Creates a greater-than-or-equal filter: `key >= value`.
103    pub fn greater_equal<T: Serialize>(key: impl Into<String>, value: T) -> Self {
104        Self::Condition(Condition::GreaterEqual {
105            key: key.into(),
106            value: serde_json::to_value(value)
107                .expect("MetadataFilter::greater_equal: value must be serializable"),
108        })
109    }
110
111    /// Creates a less-than filter: `key < value`.
112    pub fn less<T: Serialize>(key: impl Into<String>, value: T) -> Self {
113        Self::Condition(Condition::Less {
114            key: key.into(),
115            value: serde_json::to_value(value)
116                .expect("MetadataFilter::less: value must be serializable"),
117        })
118    }
119
120    /// Creates a less-than-or-equal filter: `key <= value`.
121    pub fn less_equal<T: Serialize>(key: impl Into<String>, value: T) -> Self {
122        Self::Condition(Condition::LessEqual {
123            key: key.into(),
124            value: serde_json::to_value(value)
125                .expect("MetadataFilter::less_equal: value must be serializable"),
126        })
127    }
128
129    /// Creates an existence filter: the key must be present.
130    pub fn exists(key: impl Into<String>) -> Self {
131        Self::Condition(Condition::Exists { key: key.into() })
132    }
133
134    /// Creates a non-existence filter: the key must be absent.
135    pub fn not_exists(key: impl Into<String>) -> Self {
136        Self::Condition(Condition::NotExists { key: key.into() })
137    }
138
139    /// Creates an in-set filter: `key ∈ values`.
140    pub fn in_values<T, I>(key: impl Into<String>, values: I) -> Self
141    where
142        T: Serialize,
143        I: IntoIterator<Item = T>,
144    {
145        let values = values
146            .into_iter()
147            .map(|v| {
148                serde_json::to_value(v)
149                    .expect("MetadataFilter::in_values: each value must be serializable")
150            })
151            .collect();
152        Self::Condition(Condition::In {
153            key: key.into(),
154            values,
155        })
156    }
157
158    /// Creates a not-in-set filter: `key ∉ values`.
159    pub fn not_in_values<T, I>(key: impl Into<String>, values: I) -> Self
160    where
161        T: Serialize,
162        I: IntoIterator<Item = T>,
163    {
164        let values = values
165            .into_iter()
166            .map(|v| {
167                serde_json::to_value(v)
168                    .expect("MetadataFilter::not_in_values: each value must be serializable")
169            })
170            .collect();
171        Self::Condition(Condition::NotIn {
172            key: key.into(),
173            values,
174        })
175    }
176
177    // ── Logical combinators ──────────────────────────────────────────────────
178
179    /// Combines `self` and `other` with a logical AND.
180    ///
181    /// If `self` is already an `And` node the new filter is appended to its
182    /// children rather than creating a new nested node.
183    #[must_use]
184    pub fn and(self, other: MetadataFilter) -> Self {
185        match self {
186            MetadataFilter::And(mut children) => {
187                children.push(other);
188                MetadataFilter::And(children)
189            }
190            _ => MetadataFilter::And(vec![self, other]),
191        }
192    }
193
194    /// Combines `self` and `other` with a logical OR.
195    ///
196    /// If `self` is already an `Or` node the new filter is appended to its
197    /// children rather than creating a new nested node.
198    #[must_use]
199    pub fn or(self, other: MetadataFilter) -> Self {
200        match self {
201            MetadataFilter::Or(mut children) => {
202                children.push(other);
203                MetadataFilter::Or(children)
204            }
205            _ => MetadataFilter::Or(vec![self, other]),
206        }
207    }
208
209    /// Wraps `self` in a logical NOT.
210    #[allow(clippy::should_implement_trait)]
211    #[must_use]
212    pub fn not(self) -> Self {
213        !self
214    }
215
216    // ── Evaluation ───────────────────────────────────────────────────────────
217
218    /// Returns `true` if `meta` satisfies this filter.
219    pub fn matches(&self, meta: &Metadata) -> bool {
220        match self {
221            MetadataFilter::Condition(cond) => cond.matches(meta),
222            MetadataFilter::And(children) => children.iter().all(|f| f.matches(meta)),
223            MetadataFilter::Or(children) => children.iter().any(|f| f.matches(meta)),
224            MetadataFilter::Not(inner) => !inner.matches(meta),
225        }
226    }
227}
228
229impl std::ops::Not for MetadataFilter {
230    type Output = MetadataFilter;
231
232    fn not(self) -> Self::Output {
233        MetadataFilter::Not(Box::new(self))
234    }
235}