Skip to main content

filt_rs/
lib.rs

1//! A human-friendly filter expression language for matching your objects against
2//! user-provided queries.
3//!
4//! This crate provides a small, dependency-light filtering DSL designed for
5//! situations where your users need to describe *which* items a tool should
6//! operate on — for example which repositories to back up, which emails to
7//! restore, or which releases to download. It was originally developed for
8//! (and extracted from) the Sierra Softworks
9//! [`github-backup`](https://github.com/SierraSoftworks/github-backup) and
10//! [`mail-backup`](https://github.com/SierraSoftworks/mail-backup) projects.
11//!
12//! # Quick start
13//!
14//! Implement the [`Filterable`] trait on your type to expose the properties
15//! which may be referenced in a filter expression, then parse a [`Filter`]
16//! and evaluate it against your objects.
17//!
18//! ```
19//! use filt_rs::{Filter, FilterValue, Filterable};
20//!
21//! struct Repo {
22//!     name: &'static str,
23//!     public: bool,
24//!     stars: u32,
25//! }
26//!
27//! impl Filterable for Repo {
28//!     fn get(&self, key: &str) -> FilterValue<'_> {
29//!         match key {
30//!             "repo.name" => self.name.into(),
31//!             "repo.public" => self.public.into(),
32//!             "repo.stars" => self.stars.into(),
33//!             _ => FilterValue::Null,
34//!         }
35//!     }
36//! }
37//!
38//! # fn main() -> Result<(), filt_rs::Error> {
39//! let filter = Filter::new("repo.public && repo.stars >= 50")?;
40//!
41//! let repo = Repo { name: "git-tool", public: true, stars: 87 };
42//! assert!(filter.matches(&repo)?);
43//!
44//! let repo = Repo { name: "top-secret", public: false, stars: 3 };
45//! assert!(!filter.matches(&repo)?);
46//! # Ok(())
47//! # }
48//! ```
49//!
50//! # Filter syntax
51//!
52//! A filter is a single logical expression which is evaluated against each
53//! object, matching the object whenever the expression is
54//! [truthy](FilterValue::is_truthy).
55//!
56//! ```text
57//! repo.public && !repo.fork && repo.name in ["git-tool", "grey"]
58//! ```
59//!
60//! ## Literals
61//!
62//! | Literal    | Example                | Notes                                            |
63//! |------------|------------------------|--------------------------------------------------|
64//! | Null       | `null`                 | Also returned for properties which aren't found. |
65//! | Boolean    | `true`, `false`        |                                                  |
66//! | Number     | `123`, `123.45`        | All numbers are 64-bit floats internally.        |
67//! | String     | `"hello"`              | Escape embedded quotes with `\"`.                |
68//! | Raw string | `r"^v\d+$"`            | No escape processing. Use the hashed form `r#"..."#` (e.g. `r#"{"k":1}"#`) to embed `"`. |
69//! | Tuple      | `["a", "b"]`           | A list of literal values.                        |
70//! | Duration   | `5m`, `1h30m`, `500ms` | Requires the **`chrono`** crate feature.         |
71//!
72//! ## Properties
73//!
74//! Any other identifier (including `.` and `-` separated names like
75//! `release.prerelease` or `asset.source-code`) is treated as a property
76//! reference, and is resolved by calling [`Filterable::get`] on the target
77//! object. Note that the operator keywords below (`in`, `contains`, `like`,
78//! `matches`, etc.) are reserved and cannot be used as property names.
79//!
80//! ## Operators
81//!
82//! In order of increasing precedence:
83//!
84//! | Operator                 | Meaning                                                            |
85//! |--------------------------|--------------------------------------------------------------------|
86//! | `\|\|`                   | Logical OR (short-circuiting).                                     |
87//! | `&&`                     | Logical AND (short-circuiting).                                    |
88//! | `==`, `!=`               | Equality (strings are compared case-insensitively).                |
89//! | `>`, `>=`, `<`, `<=`     | Ordering comparisons.                                              |
90//! | `contains`               | String contains a substring, or tuple contains a value.            |
91//! | `in`                     | Inverse of `contains` (i.e. `a in b` ≡ `b contains a`).            |
92//! | `startswith`, `endswith` | String prefix/suffix tests (case-insensitive).                     |
93//! | `like`                   | Case-insensitive glob match (`*` and `?` wildcards).               |
94//! | `matches`                | Regular expression match (requires the **`regex`** crate feature). |
95//! | `+`, `-`                 | Addition and subtraction (numbers, datetimes, and durations).      |
96//! | `!`                      | Logical NOT (unary).                                               |
97//! | `(...)`                  | Grouping.                                                          |
98//!
99//! ## Case sensitivity
100//!
101//! The string operators above compare case-insensitively, folding both
102//! operands with the language's Unicode case-folding rules. Each of them
103//! (except `matches`, where the pattern author controls casing with `(?i)`)
104//! has a case-*sensitive* variant with a `_cs` suffix which compares strings
105//! exactly as written: `contains_cs`, `in_cs`, `startswith_cs`,
106//! `endswith_cs`, and `like_cs`. They sit at the same precedence as their
107//! case-insensitive counterparts, and tuple membership through `contains_cs`
108//! and `in_cs` compares the tuple's elements case-sensitively too.
109//!
110//! ```text
111//! branch.name startswith_cs "Feat/" && "Alice" in_cs branch.reviewers
112//! ```
113//!
114//! ## Pattern matching
115//!
116//! The `like` operator matches a string against a glob pattern. `*` matches
117//! any sequence of characters (including none), `?` matches exactly one
118//! character, and a backslash makes the following character literal (`\*`,
119//! `\?`, `\\`); character classes like `[a-z]` are **not** supported. Like
120//! the rest of the language, matching is case-insensitive: both the pattern
121//! and the input are folded using the language's Unicode case-folding rules,
122//! including multi-character folds (`"groß" like "*ss"` holds, and `?`
123//! counts folded characters, so `ß` counts as two). The `like_cs` variant
124//! matches case-sensitively instead, with no folding at all.
125//!
126//! ```
127//! use filt_rs::{Filter, FilterValue, Filterable};
128//!
129//! struct Branch(&'static str);
130//!
131//! impl Filterable for Branch {
132//!     fn get(&self, key: &str) -> FilterValue<'_> {
133//!         match key {
134//!             "branch.name" => self.0.into(),
135//!             _ => FilterValue::Null,
136//!         }
137//!     }
138//! }
139//!
140//! # fn main() -> Result<(), filt_rs::Error> {
141//! let filter = Filter::new(r#"branch.name like "feat/*""#)?;
142//! assert!(filter.matches(&Branch("feat/login"))?);
143//! assert!(filter.matches(&Branch("FEAT/LOGIN"))?);
144//! assert!(!filter.matches(&Branch("fix/typo"))?);
145//! # Ok(())
146//! # }
147//! ```
148//!
149//! With the **`regex`** crate feature enabled, the `matches` operator tests a
150//! string against a regular expression (as implemented by the
151//! [regex](https://docs.rs/regex) crate). Raw strings (`r"..."`) are the most
152//! convenient way to write these, since they perform no escape processing.
153//! Unlike the rest of the language, regular expressions are case-sensitive as
154//! written (use `(?i)` to ignore case) and unanchored (use `^` and `$` to
155//! anchor the match).
156//!
157//! ```
158//! # use filt_rs::{Filter, FilterValue, Filterable};
159//! # struct Branch(&'static str);
160//! # impl Filterable for Branch {
161//! #     fn get(&self, key: &str) -> FilterValue<'_> {
162//! #         match key {
163//! #             "branch.name" => self.0.into(),
164//! #             _ => FilterValue::Null,
165//! #         }
166//! #     }
167//! # }
168//! # fn main() -> Result<(), filt_rs::Error> {
169//! # #[cfg(feature = "regex")]
170//! # {
171//! let filter = Filter::new(r#"branch.name matches r"^release/v\d+(\.\d+){2}$""#)?;
172//! assert!(filter.matches(&Branch("release/v1.2.3"))?);
173//! assert!(!filter.matches(&Branch("release/v1.2"))?);
174//! # }
175//! # Ok(())
176//! # }
177//! ```
178//!
179//! Both operators require their pattern to be a string literal: the pattern
180//! is compiled once when the filter is parsed (with invalid regular
181//! expressions reported as friendly [`Filter::new`] errors), and evaluation
182//! performs no pattern-related heap allocation. Glob evaluation is fully
183//! allocation-free, while regex evaluation is *amortized* allocation-free
184//! (the regex engine lazily allocates per-thread scratch space on first use
185//! and reuses it thereafter). Only string values can match a pattern: tuples
186//! match when any of their string elements match, while `null`, booleans, and
187//! numbers never match — even against `like "*"`.
188//!
189//! ## Arithmetic
190//!
191//! The `+` and `-` operators bind tighter than comparisons, so
192//! `a + b > c` is read as `(a + b) > c`. Numbers may be added to and
193//! subtracted from one another, while any unsupported combination of operand
194//! types evaluates to `null` (consistent with the language's lenient
195//! comparison semantics). There is no unary minus: write `0 - 5` to produce a
196//! negative value.
197//!
198//! ```
199//! # use filt_rs::Filter;
200//! # struct Nothing;
201//! # impl filt_rs::Filterable for Nothing {
202//! #     fn get(&self, _key: &str) -> filt_rs::FilterValue<'_> { filt_rs::FilterValue::Null }
203//! # }
204//! # fn main() -> Result<(), filt_rs::Error> {
205//! let filter = Filter::new("1 + 2 - 4 < 0")?;
206//! assert!(filter.matches(&Nothing)?);
207//! # Ok(())
208//! # }
209//! ```
210//!
211//! Note that a `-` *inside* a property name remains part of that name (so
212//! `asset.source-code` is a single property), while a `-` which starts a new
213//! token is the subtraction operator: `asset.size - 5` subtracts, but
214//! `asset.size-5` references a property named `asset.size-5`.
215//!
216//! ## Functions
217//!
218//! Filters may call built-in functions using the familiar `name(args...)`
219//! syntax. Function names and argument counts are validated when the filter
220//! is parsed, so typos fail fast with a friendly error rather than at
221//! evaluation time.
222//!
223//! | Function       | Result                                                                                   |
224//! |----------------|------------------------------------------------------------------------------------------|
225//! | `now()`        | The current UTC time, evaluated at each [`Filter::matches`] call. Requires **`chrono`**.  |
226//! | `ago(duration)` | The current UTC time minus `duration`, equivalent to `now() - duration` (e.g. `ago(30m)`). Requires **`chrono`**. |
227//! | `datetime(string)` | An absolute UTC time parsed from an ISO 8601 string (e.g. `datetime("2026-03-12T12:00:00")`). Requires **`chrono`**. |
228//! | `trim(string)` | Its string argument with leading and trailing whitespace removed (`null` for non-strings). |
229//!
230//! `trim` evaluates lazily and without allocating when its argument is a
231//! borrowed string (it returns a sub-slice of the original), making it cheap
232//! to normalise user input before comparing it:
233//!
234//! ```
235//! # use filt_rs::{Filter, FilterValue, Filterable};
236//! # struct Issue(&'static str);
237//! # impl Filterable for Issue {
238//! #     fn get(&self, key: &str) -> FilterValue<'_> {
239//! #         match key {
240//! #             "issue.title" => self.0.into(),
241//! #             _ => FilterValue::Null,
242//! #         }
243//! #     }
244//! # }
245//! # fn main() -> Result<(), filt_rs::Error> {
246//! let filter = Filter::new(r#"trim(issue.title) == "Release notes""#)?;
247//! assert!(filter.matches(&Issue("  Release notes\n"))?);
248//! # Ok(())
249//! # }
250//! ```
251//!
252//! You can extend the language with your own functions by implementing the
253//! [`Function`] trait and constructing filters with
254//! [`Filter::with_functions`], which makes them available alongside the
255//! built-in set.
256//!
257//! ## Datetimes and durations
258//!
259//! With the **`chrono`** crate feature enabled, filters can work with points
260//! in time and spans of time:
261//!
262//! - Duration literals are written as a number immediately followed by a
263//!   unit — `ms` (milliseconds), `s` (seconds), `m` (minutes), `h` (hours),
264//!   `d` (days), or `w` (weeks) — and may chain several segments together:
265//!   `90s`, `5m`, `1h30m`, `500ms`.
266//! - [`Filterable::get`] implementations can return
267//!   [`FilterValue::DateTime`](FilterValue) values (e.g. from
268//!   [`chrono::DateTime<Utc>`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html)
269//!   or [`std::time::SystemTime`]).
270//! - Datetimes and durations support ordering comparisons against values of
271//!   the same type, and arithmetic via `+` and `-`:
272//!   `DateTime ± Duration → DateTime`, `DateTime - DateTime → Duration`, and
273//!   `Duration ± Duration → Duration`.
274//! - Datetimes are always truthy, while durations are truthy if (and only
275//!   if) they are non-zero.
276//!
277//! This makes relative-time filters pleasantly concise:
278//!
279//! ```text
280//! event.timestamp > now() - 5m
281//! event.timestamp > ago(5m)
282//! event.timestamp > datetime("2026-03-12T12:00:00")
283//! ```
284//!
285//! Without the `chrono` feature, duration literals and `now()` are still
286//! recognised by the parser but produce a friendly error explaining that the
287//! feature must be enabled.
288//!
289//! # Crate features
290//!
291//! - **`regex`** — enables the `matches` regular expression operator (adds a
292//!   dependency on the [regex](https://docs.rs/regex) crate). Without this
293//!   feature, filters using `matches` fail to parse with an error explaining
294//!   how to enable it.
295//! - **`chrono`** — adds datetime and duration support: the
296//!   [`FilterValue::DateTime`](FilterValue) and
297//!   [`FilterValue::Duration`](FilterValue) variants, duration literals such
298//!   as `5m` and `1h30m`, the `now()`, `ago()`, and `datetime()` functions, and
299//!   temporal arithmetic and
300//!   comparisons (see [Datetimes and durations](#datetimes-and-durations)).
301//!
302//! - **`secrecy`** — adds a `FilterValue::Secret` variant backed by the
303//!   [`secrecy`](https://docs.rs/secrecy) crate's `SecretString`. Secret values
304//!   behave exactly like strings in every comparison operation, but are always
305//!   formatted as `[REDACTED]`, making it impossible to leak them through
306//!   logging. See `FilterValue::secret` for details.
307//!
308//!   ```
309//!   # #[cfg(feature = "secrecy")] {
310//!   use filt_rs::{Filter, FilterValue, Filterable};
311//!
312//!   struct Credentials {
313//!       password: secrecy::SecretString,
314//!   }
315//!
316//!   impl Filterable for Credentials {
317//!       fn get(&self, key: &str) -> FilterValue<'_> {
318//!           match key {
319//!               "password" => self.password.clone().into(),
320//!               _ => FilterValue::Null,
321//!           }
322//!       }
323//!   }
324//!
325//!   let creds = Credentials { password: "hunter2".into() };
326//!
327//!   // Secrets compare exactly like strings within filter expressions...
328//!   let filter = Filter::new(r#"password == "Hunter2""#).unwrap();
329//!   assert!(filter.matches(&creds).unwrap());
330//!
331//!   // ...but they are always redacted when formatted.
332//!   assert_eq!(creds.get("password").to_string(), "[REDACTED]");
333//!   # }
334//!   ```
335//!
336//! - **`serde`** — implements [`serde::Deserialize`] for [`Filter`], allowing
337//!   filters to be parsed directly out of configuration files (a missing or
338//!   `null` value deserializes to the match-everything `true` filter).
339//!
340//! - **`visitor`** — exposes the parsed expression tree and a visitor
341//!   interface: the `Expr` AST, the `ExprVisitor` trait, the `BinaryOperator`,
342//!   `LogicalOperator`, and `UnaryOperator` enums, and the `Filter::visit`
343//!   method. This lets downstream crates walk and transform a filter — for
344//!   example to collect the properties it references, estimate its cost, or
345//!   translate it into another query language. See the `property_collector`
346//!   example (`cargo run --example property_collector --features visitor`) for
347//!   a worked illustration.
348//!
349//! [`serde::Deserialize`]: https://docs.rs/serde/latest/serde/trait.Deserialize.html
350
351#![warn(missing_docs)]
352#![deny(rustdoc::broken_intra_doc_links)]
353#![doc(
354    html_logo_url = "https://raw.githubusercontent.com/SierraSoftworks/filters/main/assets/icon.svg",
355    html_favicon_url = "https://raw.githubusercontent.com/SierraSoftworks/filters/main/assets/icon.svg"
356)]
357
358mod case_sensitivity;
359mod expr;
360mod functions;
361mod interpreter;
362mod lexer;
363mod location;
364mod operator;
365mod parser;
366mod pattern;
367mod token;
368mod value;
369
370use std::{fmt::Display, pin::Pin, ptr::NonNull, sync::Arc};
371
372use functions::base_functions;
373use interpreter::FilterContext;
374
375pub use functions::Function;
376pub use human_errors::Error;
377pub use value::{FilterValue, Filterable};
378
379// The expression-visitor API is gated behind the `visitor` feature. The `Expr`
380// AST and `ExprVisitor` trait are always needed internally (the interpreter is
381// itself a visitor), so they are imported privately when the feature is off and
382// re-exported publicly when it is on.
383#[cfg(feature = "visitor")]
384pub use expr::{Expr, ExprVisitor};
385#[cfg(not(feature = "visitor"))]
386use expr::{Expr, ExprVisitor};
387
388#[cfg(feature = "visitor")]
389pub use operator::{BinaryOperator, LogicalOperator, UnaryOperator};
390#[cfg(all(feature = "visitor", feature = "regex"))]
391pub use pattern::CompiledRegex;
392#[cfg(feature = "visitor")]
393pub use pattern::Glob;
394
395/// A parsed filter expression which can be evaluated against [`Filterable`] objects.
396///
397/// A `Filter` is constructed from a textual filter expression using
398/// [`Filter::new`], which tokenizes and parses the expression up-front so that
399/// it can be cheaply evaluated against any number of objects using
400/// [`Filter::matches`].
401///
402/// ```
403/// use filt_rs::{Filter, FilterValue, Filterable};
404///
405/// struct Server {
406///     hostname: &'static str,
407///     port: u16,
408/// }
409///
410/// impl Filterable for Server {
411///     fn get(&self, key: &str) -> FilterValue<'_> {
412///         match key {
413///             "hostname" => self.hostname.into(),
414///             "port" => self.port.into(),
415///             _ => FilterValue::Null,
416///         }
417///     }
418/// }
419///
420/// # fn main() -> Result<(), filt_rs::Error> {
421/// let filter = Filter::new(r#"hostname startswith "web" && port == 443"#)?;
422///
423/// assert!(filter.matches(&Server { hostname: "web-01", port: 443 })?);
424/// assert!(!filter.matches(&Server { hostname: "db-01", port: 5432 })?);
425/// # Ok(())
426/// # }
427/// ```
428///
429/// The default filter is the expression `true`, which matches every object:
430///
431/// ```
432/// # use filt_rs::{Filter, FilterValue, Filterable};
433/// # struct Anything;
434/// # impl Filterable for Anything {
435/// #     fn get(&self, _key: &str) -> FilterValue<'_> { FilterValue::Null }
436/// # }
437/// let filter = Filter::default();
438/// assert_eq!(filter.raw(), "true");
439/// assert!(filter.matches(&Anything).unwrap());
440/// ```
441pub struct Filter {
442    #[allow(clippy::box_collection)]
443    filter: Pin<Box<String>>,
444    ast: Expr<'static>,
445    /// The functions the expression was parsed against. Retained so the filter
446    /// can be re-parsed (e.g. when cloned) with the same set available, and to
447    /// keep any [`Function`]s referenced by the AST alive.
448    functions: Arc<[Arc<dyn Function>]>,
449}
450
451impl Filter {
452    /// Parses the provided filter expression, returning a reusable `Filter`.
453    ///
454    /// The expression is tokenized and parsed eagerly, so any syntax errors
455    /// are reported here rather than at evaluation time. Errors include the
456    /// location of the problem and guidance on how to correct it.
457    ///
458    /// ```
459    /// use filt_rs::Filter;
460    ///
461    /// let filter = Filter::new("size > 100 && !archived").unwrap();
462    /// assert_eq!(filter.raw(), "size > 100 && !archived");
463    ///
464    /// let error = Filter::new("size >").unwrap_err();
465    /// assert!(error.to_string().contains("end of your filter expression"));
466    /// ```
467    pub fn new<S: Into<String>>(filter: S) -> Result<Self, Error> {
468        Self::build(filter.into(), base_functions())
469    }
470
471    /// Parses the provided filter expression with additional [`Function`]s
472    /// available, returning a reusable `Filter`.
473    ///
474    /// The supplied functions are made available *in addition to* the built-in
475    /// base set (which always takes precedence on a name collision), letting you
476    /// extend the filter language with your own helpers. The returned filter
477    /// remembers its function set, so [cloning](Clone) it preserves the custom
478    /// functions.
479    ///
480    /// ```
481    /// use std::borrow::Cow;
482    /// use std::sync::Arc;
483    /// use filt_rs::{Filter, FilterValue, Filterable, Function};
484    ///
485    /// /// A `reverse(string)` function which reverses its argument's characters.
486    /// struct Reverse;
487    ///
488    /// impl Function for Reverse {
489    ///     fn name(&self) -> &str {
490    ///         "reverse"
491    ///     }
492    ///
493    ///     fn arity(&self) -> usize {
494    ///         1
495    ///     }
496    ///
497    ///     fn call<'a>(&self, args: &[Cow<'a, FilterValue<'a>>]) -> Cow<'a, FilterValue<'a>> {
498    ///         match args[0].as_ref() {
499    ///             FilterValue::String(s) => {
500    ///                 Cow::Owned(FilterValue::String(s.chars().rev().collect::<String>().into()))
501    ///             }
502    ///             _ => Cow::Owned(FilterValue::Null),
503    ///         }
504    ///     }
505    /// }
506    ///
507    /// struct Word(&'static str);
508    ///
509    /// impl Filterable for Word {
510    ///     fn get(&self, key: &str) -> FilterValue<'_> {
511    ///         match key {
512    ///             "word" => self.0.into(),
513    ///             _ => FilterValue::Null,
514    ///         }
515    ///     }
516    /// }
517    ///
518    /// # fn main() -> Result<(), filt_rs::Error> {
519    /// let custom: [Arc<dyn Function>; 1] = [Arc::new(Reverse)];
520    /// let filter = Filter::with_functions(r#"reverse(word) == "olleh""#, custom)?;
521    /// assert!(filter.matches(&Word("hello"))?);
522    ///
523    /// // Built-in functions remain available alongside your own.
524    /// let custom: [Arc<dyn Function>; 1] = [Arc::new(Reverse)];
525    /// let filter = Filter::with_functions(r#"trim(word) == "hi""#, custom)?;
526    /// assert!(filter.matches(&Word(" hi "))?);
527    /// # Ok(())
528    /// # }
529    /// ```
530    pub fn with_functions<S, F>(filter: S, functions: F) -> Result<Self, Error>
531    where
532        S: Into<String>,
533        F: IntoIterator<Item = Arc<dyn Function>>,
534    {
535        // The base set comes first so that, on a name collision, a built-in
536        // function takes precedence over a user-supplied one.
537        let mut combined = base_functions().to_vec();
538        combined.extend(functions);
539        Self::build(filter.into(), combined.into())
540    }
541
542    fn build(filter: String, functions: Arc<[Arc<dyn Function>]>) -> Result<Self, Error> {
543        // The AST borrows string slices from the filter expression itself. Pinning
544        // the boxed string keeps those borrows valid for the lifetime of this
545        // struct without re-allocating the lexemes.
546        let filter = Box::new(filter);
547        let filter_ptr = NonNull::from(&filter);
548        let pinned = Box::into_pin(filter);
549
550        let tokens = lexer::Scanner::new(unsafe { filter_ptr.as_ref() });
551        let ast = parser::Parser::parse(tokens.into_iter(), &functions)?;
552        Ok(Self {
553            filter: pinned,
554            ast,
555            functions,
556        })
557    }
558
559    /// Evaluates this filter against the provided object, returning whether it matched.
560    ///
561    /// The object's properties are resolved through its [`Filterable::get`]
562    /// implementation, and the filter matches when the expression evaluates to
563    /// a [truthy](FilterValue::is_truthy) value.
564    ///
565    /// ```
566    /// use filt_rs::{Filter, FilterValue, Filterable};
567    ///
568    /// struct Message(&'static str);
569    ///
570    /// impl Filterable for Message {
571    ///     fn get(&self, key: &str) -> FilterValue<'_> {
572    ///         match key {
573    ///             "subject" => self.0.into(),
574    ///             _ => FilterValue::Null,
575    ///         }
576    ///     }
577    /// }
578    ///
579    /// # fn main() -> Result<(), filt_rs::Error> {
580    /// let filter = Filter::new(r#"subject contains "invoice""#)?;
581    /// assert!(filter.matches(&Message("Invoice #123"))?);
582    /// assert!(!filter.matches(&Message("Weekly newsletter"))?);
583    /// # Ok(())
584    /// # }
585    /// ```
586    pub fn matches<T: Filterable>(&self, target: &T) -> Result<bool, Error> {
587        Ok(self.visit(&mut FilterContext::new(target)).is_truthy())
588    }
589
590    /// Walks this filter's parsed expression tree with a custom
591    /// [`ExprVisitor`], returning whatever the visitor produces.
592    ///
593    /// This is the public entry point for inspecting or transforming the
594    /// structure of a filter — for instance to collect the properties it
595    /// references, estimate its cost, or translate it into another query
596    /// language. The visitor is handed the root of the tree and is responsible
597    /// for recursing into child nodes (typically by calling
598    /// [`ExprVisitor::visit_expr`] on them).
599    ///
600    /// This method (along with the [`Expr`] and [`ExprVisitor`] types it
601    /// operates on, and the [`BinaryOperator`], [`LogicalOperator`], and
602    /// [`UnaryOperator`] enums) is only available when the **`visitor`** crate
603    /// feature is enabled.
604    ///
605    /// ```
606    /// use filt_rs::{
607    ///     BinaryOperator, Expr, ExprVisitor, Filter, FilterValue, Function, Glob,
608    ///     LogicalOperator, UnaryOperator,
609    /// };
610    ///
611    /// /// Counts how many nodes a filter's expression tree contains.
612    /// struct NodeCounter;
613    ///
614    /// impl<'a> ExprVisitor<'a, usize> for NodeCounter {
615    ///     fn visit_literal(&mut self, _value: &FilterValue) -> usize { 1 }
616    ///     fn visit_property(&mut self, _name: &str) -> usize { 1 }
617    ///     fn visit_function_call(&mut self, _function: &dyn Function, args: &[Expr]) -> usize {
618    ///         1 + args.iter().map(|arg| self.visit_expr(arg)).sum::<usize>()
619    ///     }
620    ///     fn visit_binary(&mut self, l: &'a Expr<'a>, _op: BinaryOperator, r: &'a Expr<'a>) -> usize {
621    ///         1 + self.visit_expr(l) + self.visit_expr(r)
622    ///     }
623    ///     fn visit_logical(&mut self, l: &'a Expr<'a>, _op: LogicalOperator, r: &'a Expr<'a>) -> usize {
624    ///         1 + self.visit_expr(l) + self.visit_expr(r)
625    ///     }
626    ///     fn visit_unary(&mut self, _op: UnaryOperator, r: &'a Expr<'a>) -> usize {
627    ///         1 + self.visit_expr(r)
628    ///     }
629    ///     fn visit_like(&mut self, l: &'a Expr<'a>, _glob: &Glob) -> usize {
630    ///         1 + self.visit_expr(l)
631    ///     }
632    ///     # #[cfg(feature = "regex")]
633    ///     fn visit_matches(&mut self, l: &'a Expr<'a>, _re: &filt_rs::CompiledRegex) -> usize {
634    ///         1 + self.visit_expr(l)
635    ///     }
636    /// }
637    ///
638    /// # fn main() -> Result<(), filt_rs::Error> {
639    /// let filter = Filter::new("repo.public && repo.stars >= 50")?;
640    /// // (&&) + property + (>=) + property + literal → 5 nodes.
641    /// assert_eq!(filter.visit(&mut NodeCounter), 5);
642    /// # Ok(())
643    /// # }
644    /// ```
645    #[cfg(feature = "visitor")]
646    pub fn visit<'this, V, T>(&'this self, visitor: &mut V) -> T
647    where
648        V: ExprVisitor<'this, T>,
649    {
650        // The AST borrows from the pinned filter string, so its `'static`
651        // lifetime is really tied to `self`. Handing the visitor `&'this`
652        // borrows narrows that back down to the lifetime of this call.
653        visitor.visit_expr(&self.ast)
654    }
655
656    /// Internal expression walker used by [`Filter::matches`]. This is the same
657    /// as the public [`Filter::visit`], but remains available (crate-private)
658    /// when the `visitor` feature is disabled so that evaluation still works.
659    #[cfg(not(feature = "visitor"))]
660    pub(crate) fn visit<'this, V, T>(&'this self, visitor: &mut V) -> T
661    where
662        V: ExprVisitor<'this, T>,
663    {
664        visitor.visit_expr(&self.ast)
665    }
666
667    /// Gets the raw filter expression which was used to construct this filter.
668    ///
669    /// ```
670    /// use filt_rs::Filter;
671    ///
672    /// let filter = Filter::new("name == \"demo\"").unwrap();
673    /// assert_eq!(filter.raw(), "name == \"demo\"");
674    /// ```
675    pub fn raw(&self) -> &str {
676        &self.filter
677    }
678}
679
680impl Default for Filter {
681    /// Returns the match-everything filter `true`.
682    fn default() -> Self {
683        Self {
684            filter: Box::pin("true".to_string()),
685            ast: Expr::Literal(FilterValue::Bool(true)),
686            functions: base_functions(),
687        }
688    }
689}
690
691impl Clone for Filter {
692    fn clone(&self) -> Self {
693        // Re-parse against the same function set so that any custom functions
694        // registered with [`Filter::with_functions`] remain available.
695        Self::build(self.raw().to_string(), Arc::clone(&self.functions)).expect("clone filter")
696    }
697}
698
699impl PartialEq for Filter {
700    fn eq(&self, other: &Self) -> bool {
701        self.ast == other.ast
702    }
703}
704
705impl std::fmt::Debug for Filter {
706    /// Formats the filter as its parsed expression tree, which can be useful
707    /// when debugging operator precedence issues.
708    ///
709    /// ```
710    /// use filt_rs::Filter;
711    ///
712    /// let filter = Filter::new("a || b && c").unwrap();
713    /// assert_eq!(format!("{filter:?}"), "(|| (property a) (&& (property b) (property c)))");
714    /// ```
715    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
716        write!(f, "{:?}", self.ast)
717    }
718}
719
720impl Display for Filter {
721    /// Formats the filter as its original raw expression.
722    ///
723    /// ```
724    /// use filt_rs::Filter;
725    ///
726    /// let filter = Filter::new("a || b").unwrap();
727    /// assert_eq!(filter.to_string(), "a || b");
728    /// ```
729    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
730        write!(f, "{}", self.raw())
731    }
732}
733
734#[cfg(feature = "serde")]
735impl serde::Serialize for Filter {
736    /// Serializes a `Filter` as its raw expression string.
737    ///
738    /// ```
739    /// use filt_rs::Filter;
740    ///
741    /// let filter = Filter::new("a || b").unwrap();
742    /// let json = serde_json::to_string(&filter).unwrap();
743    /// assert_eq!(json, r#""a || b""#);
744    /// ```
745    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
746    where
747        S: serde::Serializer,
748    {
749        serializer.serialize_str(self.raw())
750    }
751}
752
753#[cfg(feature = "serde")]
754impl<'de> serde::Deserialize<'de> for Filter {
755    /// Deserializes a `Filter` from a string containing a filter expression.
756    ///
757    /// Missing or `null` values are deserialized as the match-everything
758    /// filter `true`, making it easy to use optional filter fields within
759    /// your configuration structures.
760    ///
761    /// ```
762    /// use filt_rs::Filter;
763    ///
764    /// #[derive(serde::Deserialize)]
765    /// struct Config {
766    ///     #[serde(default)]
767    ///     filter: Filter,
768    /// }
769    ///
770    /// let config: Config = serde_json::from_str(r#"{"filter": "!repo.fork"}"#).unwrap();
771    /// assert_eq!(config.filter.raw(), "!repo.fork");
772    ///
773    /// let config: Config = serde_json::from_str("{}").unwrap();
774    /// assert_eq!(config.filter.raw(), "true");
775    /// ```
776    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
777    where
778        D: serde::Deserializer<'de>,
779    {
780        struct FilterVisitor;
781
782        impl<'de> serde::de::Visitor<'de> for FilterVisitor {
783            type Value = Filter;
784
785            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
786                formatter.write_str("a valid filter expression")
787            }
788
789            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
790            where
791                E: serde::de::Error,
792            {
793                Filter::new(v).map_err(serde::de::Error::custom)
794            }
795
796            fn visit_none<E>(self) -> Result<Self::Value, E>
797            where
798                E: serde::de::Error,
799            {
800                Filter::new("true").map_err(serde::de::Error::custom)
801            }
802
803            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
804            where
805                D: serde::Deserializer<'de>,
806            {
807                deserializer.deserialize_str(self)
808            }
809        }
810
811        deserializer.deserialize_option(FilterVisitor)
812    }
813}
814
815#[cfg(test)]
816mod tests {
817    use rstest::rstest;
818
819    use super::*;
820
821    struct TestObject {
822        name: String,
823        age: i32,
824        alive: bool,
825        tags: Vec<&'static str>,
826    }
827
828    impl Default for TestObject {
829        fn default() -> Self {
830            Self {
831                name: "John Doe".to_string(),
832                age: 30,
833                alive: true,
834                tags: vec!["red", "black"],
835            }
836        }
837    }
838
839    impl Filterable for TestObject {
840        fn get(&self, property: &str) -> FilterValue<'_> {
841            match property {
842                "name" => self.name.clone().into(),
843                "age" => self.age.into(),
844                "alive" => self.alive.into(),
845                "tags" => self
846                    .tags
847                    .iter()
848                    .cloned()
849                    .map(|v| v.into())
850                    .collect::<Vec<FilterValue<'_>>>()
851                    .into(),
852                _ => FilterValue::Null,
853            }
854        }
855    }
856
857    #[rstest]
858    #[case("name == \"John Doe\"", true)]
859    #[case("name != \"John Doe\"", false)]
860    #[case("name == \"Jane Doe\"", false)]
861    #[case("name != \"Jane Doe\"", true)]
862    #[case("name startswith \"John\"", true)]
863    #[case("name startswith \"Jane\"", false)]
864    #[case("name endswith \"Doe\"", true)]
865    #[case("name endswith \"Smith\"", false)]
866    #[case("age == 30", true)]
867    #[case("age != 30", false)]
868    #[case("age == 31", false)]
869    #[case("age != 31", true)]
870    #[case("age > 31", false)]
871    #[case("age < 31", true)]
872    #[case("age >= 30", true)]
873    #[case("age <= 30", true)]
874    #[case("tags == [\"red\",\"black\"]", true)]
875    #[case("tags != [\"red\",\"black\"]", false)]
876    #[case("tags == [\"blue\"]", false)]
877    #[case("tags contains \"red\"", true)]
878    #[case("tags contains \"blue\"", false)]
879    #[case("\"red\" in tags", true)]
880    #[case("\"blue\" in tags", false)]
881    fn case_sensitive_filtering(#[case] filter: &str, #[case] matches: bool) {
882        let obj = TestObject::default();
883
884        assert_eq!(
885            Filter::new(filter)
886                .expect("parse filter")
887                .matches(&obj)
888                .expect("run filter"),
889            matches
890        );
891    }
892
893    #[rstest]
894    #[case("name == \"john doe\"", true)]
895    #[case("name != \"john doe\"", false)]
896    #[case("name == \"jane doe\"", false)]
897    #[case("name != \"jane doe\"", true)]
898    #[case("name startswith \"john\"", true)]
899    #[case("name startswith \"jane\"", false)]
900    #[case("name endswith \"doe\"", true)]
901    #[case("name endswith \"smith\"", false)]
902    #[case("\"RED\" in tags", true)]
903    #[case("\"BLUE\" in tags", false)]
904    fn case_insensitive_filtering(#[case] filter: &str, #[case] matches: bool) {
905        let obj = TestObject::default();
906
907        assert_eq!(
908            Filter::new(filter)
909                .expect("parse filter")
910                .matches(&obj)
911                .expect("run filter"),
912            matches
913        );
914    }
915
916    #[rstest]
917    #[case("name == \"John Doe\" && age == 30", true)]
918    #[case("name == \"John Doe\" && age == 31", false)]
919    #[case("name == \"Jane Doe\" && age == 30", false)]
920    #[case("name == \"John Doe\" || age == 30", true)]
921    #[case("name == \"John Doe\" || age == 31", true)]
922    #[case("name == \"Jane Doe\" || age == 30", true)]
923    #[case("name == \"Jane Doe\" || age == 31", false)]
924    fn binary_operator_filtering(#[case] filter: &str, #[case] matches: bool) {
925        let obj = TestObject::default();
926
927        assert_eq!(
928            Filter::new(filter)
929                .expect("parse filter")
930                .matches(&obj)
931                .expect("run filter"),
932            matches
933        );
934    }
935
936    #[rstest]
937    #[case("alive", true)]
938    #[case("!alive", false)]
939    #[case("name && age", true)]
940    #[case("name && !age", false)]
941    fn logical_operator_filtering(#[case] filter: &str, #[case] matches: bool) {
942        let obj = TestObject::default();
943
944        assert_eq!(
945            Filter::new(filter)
946                .expect("parse filter")
947                .matches(&obj)
948                .expect("run filter"),
949            matches
950        );
951    }
952
953    #[test]
954    fn default_filter_matches_everything() {
955        let filter = Filter::default();
956        assert_eq!(filter.raw(), "true");
957        assert!(filter.matches(&TestObject::default()).expect("run filter"));
958    }
959
960    #[test]
961    fn display_round_trips_the_raw_expression() {
962        let filter = Filter::new("age >= 30 && alive").expect("parse filter");
963        assert_eq!(filter.to_string(), "age >= 30 && alive");
964        assert_eq!(filter.raw(), "age >= 30 && alive");
965    }
966
967    #[test]
968    fn clone_preserves_the_raw_expression_and_behaviour() {
969        let filter = Filter::new("age >= 30 && alive").expect("parse filter");
970        let clone = filter.clone();
971
972        assert_eq!(clone.raw(), filter.raw());
973        assert_eq!(clone, filter);
974        assert_eq!(
975            clone.matches(&TestObject::default()).expect("run filter"),
976            filter.matches(&TestObject::default()).expect("run filter"),
977        );
978    }
979
980    #[test]
981    fn equal_filters_compare_equal() {
982        let lhs = Filter::new("age >= 30 && alive").expect("parse filter");
983        let rhs = Filter::new("age >= 30 && alive").expect("parse filter");
984        assert_eq!(lhs, rhs);
985    }
986
987    #[test]
988    fn different_filters_compare_unequal() {
989        let lhs = Filter::new("age >= 30").expect("parse filter");
990        let rhs = Filter::new("age >= 31").expect("parse filter");
991        assert_ne!(lhs, rhs);
992    }
993
994    #[rstest]
995    #[case("age >")]
996    #[case("(alive")]
997    #[case("name = \"John\"")]
998    #[case("\"unterminated")]
999    fn invalid_filters_report_errors(#[case] filter: &str) {
1000        assert!(Filter::new(filter).is_err());
1001    }
1002
1003    #[cfg(feature = "serde")]
1004    mod serde_tests {
1005        use super::*;
1006
1007        #[derive(serde::Serialize, serde::Deserialize)]
1008        struct Config {
1009            #[serde(default)]
1010            filter: Filter,
1011        }
1012
1013        #[test]
1014        fn deserializes_a_filter_expression() {
1015            let config: Config =
1016                serde_json::from_str(r#"{"filter": "age > 21 && alive"}"#).expect("deserialize");
1017            assert_eq!(config.filter.raw(), "age > 21 && alive");
1018            assert!(
1019                config
1020                    .filter
1021                    .matches(&TestObject::default())
1022                    .expect("run filter")
1023            );
1024        }
1025
1026        #[test]
1027        fn missing_filters_match_everything() {
1028            let config: Config = serde_json::from_str("{}").expect("deserialize");
1029            assert_eq!(config.filter.raw(), "true");
1030        }
1031
1032        #[test]
1033        fn null_filters_match_everything() {
1034            let config: Config = serde_json::from_str(r#"{"filter": null}"#).expect("deserialize");
1035            assert_eq!(config.filter.raw(), "true");
1036        }
1037
1038        #[test]
1039        fn invalid_filters_fail_to_deserialize() {
1040            let result: Result<Config, _> = serde_json::from_str(r#"{"filter": "age >"}"#);
1041            assert!(result.is_err());
1042        }
1043
1044        #[test]
1045        fn serializes_a_filter_as_its_raw_expression() {
1046            let filter = Filter::new("age > 21 && alive").expect("parse filter");
1047            let json = serde_json::to_string(&filter).expect("serialize");
1048            assert_eq!(json, r#""age > 21 && alive""#);
1049        }
1050
1051        #[test]
1052        fn serializes_a_filter_field() {
1053            let config = Config {
1054                filter: Filter::new("!repo.fork").expect("parse filter"),
1055            };
1056            let json = serde_json::to_string(&config).expect("serialize");
1057            assert_eq!(json, r#"{"filter":"!repo.fork"}"#);
1058        }
1059
1060        #[test]
1061        fn round_trips_through_serde() {
1062            let original: Config =
1063                serde_json::from_str(r#"{"filter": "age > 21 && alive"}"#).expect("deserialize");
1064            let json = serde_json::to_string(&original).expect("serialize");
1065            let restored: Config = serde_json::from_str(&json).expect("deserialize");
1066            assert_eq!(restored.filter.raw(), original.filter.raw());
1067        }
1068    }
1069}