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}