Skip to main content

zrx_id/id/filter/
builder.rs

1// Copyright (c) 2025-2026 Zensical and contributors
2
3// SPDX-License-Identifier: MIT
4// All contributions are certified under the DCO
5
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to
8// deal in the Software without restriction, including without limitation the
9// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10// sell copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12
13// The above copyright notice and this permission notice shall be included in
14// all copies or substantial portions of the Software.
15
16// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
19// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22// IN THE SOFTWARE.
23
24// ----------------------------------------------------------------------------
25
26//! Filter builder.
27
28use slab::Slab;
29
30use crate::id::expression::{Expression, Operator, Term};
31use crate::id::matcher::Matcher;
32
33use super::condition::Condition;
34use super::error::Result;
35use super::Filter;
36
37// ----------------------------------------------------------------------------
38// Structs
39// ----------------------------------------------------------------------------
40
41/// Filter builder.
42///
43/// This data type uses a [`Slab`] to store conditions efficiently, which makes
44/// it possible to keep indices stable when adding or removing expressions. It
45/// allows users to modify a [`Filter`] dynamically, and rebuild it on-the-fly
46/// after all modifications were made.
47#[derive(Debug, Default)]
48pub struct Builder {
49    /// Condition set.
50    conditions: Slab<Condition>,
51}
52
53// ----------------------------------------------------------------------------
54// Implementations
55// ----------------------------------------------------------------------------
56
57impl Filter {
58    /// Creates a filter builder.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use zrx_id::Filter;
64    ///
65    /// // Create filter builder
66    /// let mut builder = Filter::builder();
67    /// ```
68    #[inline]
69    #[must_use]
70    pub fn builder() -> Builder {
71        Builder::default()
72    }
73
74    /// Creates a filter builder from the filter.
75    ///
76    /// This method allows to modify an existing [`Filter`] by converting it
77    /// back into a filter builder to insert or remove expressions.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use zrx_id::Filter;
83    ///
84    /// // Create filter
85    /// let filter = Filter::default();
86    ///
87    /// // Create filter builder
88    /// let mut builder = filter.into_builder();
89    /// ```
90    #[inline]
91    #[must_use]
92    pub fn into_builder(self) -> Builder {
93        Builder { conditions: self.conditions }
94    }
95}
96
97// ----------------------------------------------------------------------------
98
99impl Builder {
100    /// Inserts an expression into the filter and returns its index.
101    ///
102    /// This method adds an [`Expression`][] to the filter builder, and returns
103    /// the index of the inserted condition, which can be used to remove it.
104    ///
105    /// Note that the expression is immediately converted into a [`Condition`]
106    /// for performance reasons, which means it cannot be recovered. If we'd
107    /// store expressions directly, removing or inserting new expressions into
108    /// the filter would mandate recompilation of all expressions.
109    ///
110    /// [`Expression`]: crate::id::expression::Expression
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// # use std::error::Error;
116    /// # fn main() -> Result<(), Box<dyn Error>> {
117    /// use zrx_id::{selector, Expression, Filter};
118    ///
119    /// // Create filter builder and insert expression
120    /// let mut builder = Filter::builder();
121    /// builder.insert(Expression::any(|expr| {
122    ///     expr.with(selector!(location = "**/*.jpg")?)?
123    ///         .with(selector!(location = "**/*.png")?)
124    /// })?);
125    /// # Ok(())
126    /// # }
127    /// ```
128    #[inline]
129    pub fn insert<T>(&mut self, expr: T) -> usize
130    where
131        T: Into<Expression>,
132    {
133        let builder = Condition::builder(expr);
134        self.conditions.insert(builder.optimize().build())
135    }
136
137    /// Removes an expression from the filter.
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// # use std::error::Error;
143    /// # fn main() -> Result<(), Box<dyn Error>> {
144    /// use zrx_id::{selector, Expression, Filter};
145    ///
146    /// // Create filter builder and insert expression
147    /// let mut builder = Filter::builder();
148    /// builder.insert(Expression::any(|expr| {
149    ///     expr.with(selector!(location = "**/*.jpg")?)?
150    ///         .with(selector!(location = "**/*.png")?)
151    /// })?);
152    ///
153    /// // Remove expression
154    /// builder.remove(0);
155    /// # Ok(())
156    /// # }
157    /// ```
158    #[inline]
159    pub fn remove(&mut self, index: usize) {
160        self.conditions.remove(index);
161    }
162
163    /// Builds the filter.
164    ///
165    /// # Errors
166    ///
167    /// Returns [`Error::Matcher`][] if the underlying matcher can't be built.
168    ///
169    /// [`Error::Matcher`]: crate::id::filter::Error::Matcher
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// # use std::error::Error;
175    /// # fn main() -> Result<(), Box<dyn Error>> {
176    /// use zrx_id::{selector, Expression, Filter};
177    ///
178    /// // Create filter builder and insert expression
179    /// let mut builder = Filter::builder();
180    /// builder.insert(Expression::any(|expr| {
181    ///     expr.with(selector!(location = "**/*.jpg")?)?
182    ///         .with(selector!(location = "**/*.png")?)
183    /// })?);
184    ///
185    /// // Create filter from builder
186    /// let filter = builder.build()?;
187    /// # Ok(())
188    /// # }
189    /// ```
190    pub fn build(self) -> Result<Filter> {
191        let mut builder = Matcher::builder();
192
193        // Initialize term mappings and negations
194        let mut mapping = Vec::with_capacity(self.conditions.len());
195        let mut negations = Vec::new();
196
197        // Add all terms of each condition to the mapping and matcher
198        for (index, condition) in &self.conditions {
199            for term in condition.terms() {
200                mapping.push(u32::try_from(index)?);
201                match term {
202                    Term::Id(id) => builder.add(id)?,
203                    Term::Selector(selector) => builder.add(selector)?,
204                };
205            }
206
207            // If the current condition contains a negation, we add its index
208            // to the list of negations, so it's always checked when matching
209            let mut iter = condition.instructions().iter();
210            if iter.any(|instruction| instruction.operator() == Operator::Not) {
211                negations.push(u32::try_from(index)?);
212            }
213        }
214
215        // Build matcher and return filter
216        Ok(Filter {
217            conditions: self.conditions,
218            negations,
219            mapping,
220            matcher: builder.build()?,
221        })
222    }
223}
224
225#[allow(clippy::must_use_candidate)]
226impl Builder {
227    /// Returns the number of expressions.
228    #[inline]
229    pub fn len(&self) -> usize {
230        self.conditions.len()
231    }
232
233    /// Returns whether there are any expressions.
234    #[inline]
235    pub fn is_empty(&self) -> bool {
236        self.conditions.is_empty()
237    }
238}