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//! | `trim(string)` | Its string argument with leading and trailing whitespace removed (`null` for non-strings). |
227//!
228//! `trim` evaluates lazily and without allocating when its argument is a
229//! borrowed string (it returns a sub-slice of the original), making it cheap
230//! to normalise user input before comparing it:
231//!
232//! ```
233//! # use filt_rs::{Filter, FilterValue, Filterable};
234//! # struct Issue(&'static str);
235//! # impl Filterable for Issue {
236//! #     fn get(&self, key: &str) -> FilterValue<'_> {
237//! #         match key {
238//! #             "issue.title" => self.0.into(),
239//! #             _ => FilterValue::Null,
240//! #         }
241//! #     }
242//! # }
243//! # fn main() -> Result<(), filt_rs::Error> {
244//! let filter = Filter::new(r#"trim(issue.title) == "Release notes""#)?;
245//! assert!(filter.matches(&Issue("  Release notes\n"))?);
246//! # Ok(())
247//! # }
248//! ```
249//!
250//! You can extend the language with your own functions by implementing the
251//! [`Function`] trait and constructing filters with
252//! [`Filter::with_functions`], which makes them available alongside the
253//! built-in set.
254//!
255//! ## Datetimes and durations
256//!
257//! With the **`chrono`** crate feature enabled, filters can work with points
258//! in time and spans of time:
259//!
260//! - Duration literals are written as a number immediately followed by a
261//!   unit — `ms` (milliseconds), `s` (seconds), `m` (minutes), `h` (hours),
262//!   `d` (days), or `w` (weeks) — and may chain several segments together:
263//!   `90s`, `5m`, `1h30m`, `500ms`.
264//! - [`Filterable::get`] implementations can return
265//!   [`FilterValue::DateTime`](FilterValue) values (e.g. from
266//!   [`chrono::DateTime<Utc>`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html)
267//!   or [`std::time::SystemTime`]).
268//! - Datetimes and durations support ordering comparisons against values of
269//!   the same type, and arithmetic via `+` and `-`:
270//!   `DateTime ± Duration → DateTime`, `DateTime - DateTime → Duration`, and
271//!   `Duration ± Duration → Duration`.
272//! - Datetimes are always truthy, while durations are truthy if (and only
273//!   if) they are non-zero.
274//!
275//! This makes relative-time filters pleasantly concise:
276//!
277//! ```text
278//! event.timestamp > now() - 5m
279//! ```
280//!
281//! Without the `chrono` feature, duration literals and `now()` are still
282//! recognised by the parser but produce a friendly error explaining that the
283//! feature must be enabled.
284//!
285//! # Crate features
286//!
287//! - **`regex`** — enables the `matches` regular expression operator (adds a
288//!   dependency on the [regex](https://docs.rs/regex) crate). Without this
289//!   feature, filters using `matches` fail to parse with an error explaining
290//!   how to enable it.
291//! - **`chrono`** — adds datetime and duration support: the
292//!   [`FilterValue::DateTime`](FilterValue) and
293//!   [`FilterValue::Duration`](FilterValue) variants, duration literals such
294//!   as `5m` and `1h30m`, the `now()` function, and temporal arithmetic and
295//!   comparisons (see [Datetimes and durations](#datetimes-and-durations)).
296//!
297//! - **`secrecy`** — adds a `FilterValue::Secret` variant backed by the
298//!   [`secrecy`](https://docs.rs/secrecy) crate's `SecretString`. Secret values
299//!   behave exactly like strings in every comparison operation, but are always
300//!   formatted as `[REDACTED]`, making it impossible to leak them through
301//!   logging. See `FilterValue::secret` for details.
302//!
303//!   ```
304//!   # #[cfg(feature = "secrecy")] {
305//!   use filt_rs::{Filter, FilterValue, Filterable};
306//!
307//!   struct Credentials {
308//!       password: secrecy::SecretString,
309//!   }
310//!
311//!   impl Filterable for Credentials {
312//!       fn get(&self, key: &str) -> FilterValue<'_> {
313//!           match key {
314//!               "password" => self.password.clone().into(),
315//!               _ => FilterValue::Null,
316//!           }
317//!       }
318//!   }
319//!
320//!   let creds = Credentials { password: "hunter2".into() };
321//!
322//!   // Secrets compare exactly like strings within filter expressions...
323//!   let filter = Filter::new(r#"password == "Hunter2""#).unwrap();
324//!   assert!(filter.matches(&creds).unwrap());
325//!
326//!   // ...but they are always redacted when formatted.
327//!   assert_eq!(creds.get("password").to_string(), "[REDACTED]");
328//!   # }
329//!   ```
330//!
331//! - **`serde`** — implements [`serde::Deserialize`] for [`Filter`], allowing
332//!   filters to be parsed directly out of configuration files (a missing or
333//!   `null` value deserializes to the match-everything `true` filter).
334//!
335//! - **`visitor`** — exposes the parsed expression tree and a visitor
336//!   interface: the `Expr` AST, the `ExprVisitor` trait, the `BinaryOperator`,
337//!   `LogicalOperator`, and `UnaryOperator` enums, and the `Filter::visit`
338//!   method. This lets downstream crates walk and transform a filter — for
339//!   example to collect the properties it references, estimate its cost, or
340//!   translate it into another query language. See the `property_collector`
341//!   example (`cargo run --example property_collector --features visitor`) for
342//!   a worked illustration.
343//!
344//! [`serde::Deserialize`]: https://docs.rs/serde/latest/serde/trait.Deserialize.html
345
346#![warn(missing_docs)]
347#![deny(rustdoc::broken_intra_doc_links)]
348#![doc(
349    html_logo_url = "https://raw.githubusercontent.com/SierraSoftworks/filters/main/assets/icon.svg",
350    html_favicon_url = "https://raw.githubusercontent.com/SierraSoftworks/filters/main/assets/icon.svg"
351)]
352
353mod case_sensitivity;
354mod expr;
355mod functions;
356mod interpreter;
357mod lexer;
358mod location;
359mod operator;
360mod parser;
361mod pattern;
362mod token;
363mod value;
364
365use std::{fmt::Display, pin::Pin, ptr::NonNull, sync::Arc};
366
367use functions::base_functions;
368use interpreter::FilterContext;
369
370pub use functions::Function;
371pub use human_errors::Error;
372pub use value::{FilterValue, Filterable};
373
374// The expression-visitor API is gated behind the `visitor` feature. The `Expr`
375// AST and `ExprVisitor` trait are always needed internally (the interpreter is
376// itself a visitor), so they are imported privately when the feature is off and
377// re-exported publicly when it is on.
378#[cfg(feature = "visitor")]
379pub use expr::{Expr, ExprVisitor};
380#[cfg(not(feature = "visitor"))]
381use expr::{Expr, ExprVisitor};
382
383#[cfg(feature = "visitor")]
384pub use operator::{BinaryOperator, LogicalOperator, UnaryOperator};
385#[cfg(all(feature = "visitor", feature = "regex"))]
386pub use pattern::CompiledRegex;
387#[cfg(feature = "visitor")]
388pub use pattern::Glob;
389
390/// A parsed filter expression which can be evaluated against [`Filterable`] objects.
391///
392/// A `Filter` is constructed from a textual filter expression using
393/// [`Filter::new`], which tokenizes and parses the expression up-front so that
394/// it can be cheaply evaluated against any number of objects using
395/// [`Filter::matches`].
396///
397/// ```
398/// use filt_rs::{Filter, FilterValue, Filterable};
399///
400/// struct Server {
401///     hostname: &'static str,
402///     port: u16,
403/// }
404///
405/// impl Filterable for Server {
406///     fn get(&self, key: &str) -> FilterValue<'_> {
407///         match key {
408///             "hostname" => self.hostname.into(),
409///             "port" => self.port.into(),
410///             _ => FilterValue::Null,
411///         }
412///     }
413/// }
414///
415/// # fn main() -> Result<(), filt_rs::Error> {
416/// let filter = Filter::new(r#"hostname startswith "web" && port == 443"#)?;
417///
418/// assert!(filter.matches(&Server { hostname: "web-01", port: 443 })?);
419/// assert!(!filter.matches(&Server { hostname: "db-01", port: 5432 })?);
420/// # Ok(())
421/// # }
422/// ```
423///
424/// The default filter is the expression `true`, which matches every object:
425///
426/// ```
427/// # use filt_rs::{Filter, FilterValue, Filterable};
428/// # struct Anything;
429/// # impl Filterable for Anything {
430/// #     fn get(&self, _key: &str) -> FilterValue<'_> { FilterValue::Null }
431/// # }
432/// let filter = Filter::default();
433/// assert_eq!(filter.raw(), "true");
434/// assert!(filter.matches(&Anything).unwrap());
435/// ```
436pub struct Filter {
437    #[allow(clippy::box_collection)]
438    filter: Pin<Box<String>>,
439    ast: Expr<'static>,
440    /// The functions the expression was parsed against. Retained so the filter
441    /// can be re-parsed (e.g. when cloned) with the same set available, and to
442    /// keep any [`Function`]s referenced by the AST alive.
443    functions: Arc<[Arc<dyn Function>]>,
444}
445
446impl Filter {
447    /// Parses the provided filter expression, returning a reusable `Filter`.
448    ///
449    /// The expression is tokenized and parsed eagerly, so any syntax errors
450    /// are reported here rather than at evaluation time. Errors include the
451    /// location of the problem and guidance on how to correct it.
452    ///
453    /// ```
454    /// use filt_rs::Filter;
455    ///
456    /// let filter = Filter::new("size > 100 && !archived").unwrap();
457    /// assert_eq!(filter.raw(), "size > 100 && !archived");
458    ///
459    /// let error = Filter::new("size >").unwrap_err();
460    /// assert!(error.to_string().contains("end of your filter expression"));
461    /// ```
462    pub fn new<S: Into<String>>(filter: S) -> Result<Self, Error> {
463        Self::build(filter.into(), base_functions())
464    }
465
466    /// Parses the provided filter expression with additional [`Function`]s
467    /// available, returning a reusable `Filter`.
468    ///
469    /// The supplied functions are made available *in addition to* the built-in
470    /// base set (which always takes precedence on a name collision), letting you
471    /// extend the filter language with your own helpers. The returned filter
472    /// remembers its function set, so [cloning](Clone) it preserves the custom
473    /// functions.
474    ///
475    /// ```
476    /// use std::borrow::Cow;
477    /// use std::sync::Arc;
478    /// use filt_rs::{Filter, FilterValue, Filterable, Function};
479    ///
480    /// /// A `reverse(string)` function which reverses its argument's characters.
481    /// struct Reverse;
482    ///
483    /// impl Function for Reverse {
484    ///     fn name(&self) -> &str {
485    ///         "reverse"
486    ///     }
487    ///
488    ///     fn arity(&self) -> usize {
489    ///         1
490    ///     }
491    ///
492    ///     fn call<'a>(&self, args: &[Cow<'a, FilterValue<'a>>]) -> Cow<'a, FilterValue<'a>> {
493    ///         match args[0].as_ref() {
494    ///             FilterValue::String(s) => {
495    ///                 Cow::Owned(FilterValue::String(s.chars().rev().collect::<String>().into()))
496    ///             }
497    ///             _ => Cow::Owned(FilterValue::Null),
498    ///         }
499    ///     }
500    /// }
501    ///
502    /// struct Word(&'static str);
503    ///
504    /// impl Filterable for Word {
505    ///     fn get(&self, key: &str) -> FilterValue<'_> {
506    ///         match key {
507    ///             "word" => self.0.into(),
508    ///             _ => FilterValue::Null,
509    ///         }
510    ///     }
511    /// }
512    ///
513    /// # fn main() -> Result<(), filt_rs::Error> {
514    /// let custom: [Arc<dyn Function>; 1] = [Arc::new(Reverse)];
515    /// let filter = Filter::with_functions(r#"reverse(word) == "olleh""#, custom)?;
516    /// assert!(filter.matches(&Word("hello"))?);
517    ///
518    /// // Built-in functions remain available alongside your own.
519    /// let custom: [Arc<dyn Function>; 1] = [Arc::new(Reverse)];
520    /// let filter = Filter::with_functions(r#"trim(word) == "hi""#, custom)?;
521    /// assert!(filter.matches(&Word(" hi "))?);
522    /// # Ok(())
523    /// # }
524    /// ```
525    pub fn with_functions<S, F>(filter: S, functions: F) -> Result<Self, Error>
526    where
527        S: Into<String>,
528        F: IntoIterator<Item = Arc<dyn Function>>,
529    {
530        // The base set comes first so that, on a name collision, a built-in
531        // function takes precedence over a user-supplied one.
532        let mut combined = base_functions().to_vec();
533        combined.extend(functions);
534        Self::build(filter.into(), combined.into())
535    }
536
537    fn build(filter: String, functions: Arc<[Arc<dyn Function>]>) -> Result<Self, Error> {
538        // The AST borrows string slices from the filter expression itself. Pinning
539        // the boxed string keeps those borrows valid for the lifetime of this
540        // struct without re-allocating the lexemes.
541        let filter = Box::new(filter);
542        let filter_ptr = NonNull::from(&filter);
543        let pinned = Box::into_pin(filter);
544
545        let tokens = lexer::Scanner::new(unsafe { filter_ptr.as_ref() });
546        let ast = parser::Parser::parse(tokens.into_iter(), &functions)?;
547        Ok(Self {
548            filter: pinned,
549            ast,
550            functions,
551        })
552    }
553
554    /// Evaluates this filter against the provided object, returning whether it matched.
555    ///
556    /// The object's properties are resolved through its [`Filterable::get`]
557    /// implementation, and the filter matches when the expression evaluates to
558    /// a [truthy](FilterValue::is_truthy) value.
559    ///
560    /// ```
561    /// use filt_rs::{Filter, FilterValue, Filterable};
562    ///
563    /// struct Message(&'static str);
564    ///
565    /// impl Filterable for Message {
566    ///     fn get(&self, key: &str) -> FilterValue<'_> {
567    ///         match key {
568    ///             "subject" => self.0.into(),
569    ///             _ => FilterValue::Null,
570    ///         }
571    ///     }
572    /// }
573    ///
574    /// # fn main() -> Result<(), filt_rs::Error> {
575    /// let filter = Filter::new(r#"subject contains "invoice""#)?;
576    /// assert!(filter.matches(&Message("Invoice #123"))?);
577    /// assert!(!filter.matches(&Message("Weekly newsletter"))?);
578    /// # Ok(())
579    /// # }
580    /// ```
581    pub fn matches<T: Filterable>(&self, target: &T) -> Result<bool, Error> {
582        Ok(self.visit(&mut FilterContext::new(target)).is_truthy())
583    }
584
585    /// Walks this filter's parsed expression tree with a custom
586    /// [`ExprVisitor`], returning whatever the visitor produces.
587    ///
588    /// This is the public entry point for inspecting or transforming the
589    /// structure of a filter — for instance to collect the properties it
590    /// references, estimate its cost, or translate it into another query
591    /// language. The visitor is handed the root of the tree and is responsible
592    /// for recursing into child nodes (typically by calling
593    /// [`ExprVisitor::visit_expr`] on them).
594    ///
595    /// This method (along with the [`Expr`] and [`ExprVisitor`] types it
596    /// operates on, and the [`BinaryOperator`], [`LogicalOperator`], and
597    /// [`UnaryOperator`] enums) is only available when the **`visitor`** crate
598    /// feature is enabled.
599    ///
600    /// ```
601    /// use filt_rs::{
602    ///     BinaryOperator, Expr, ExprVisitor, Filter, FilterValue, Function, Glob,
603    ///     LogicalOperator, UnaryOperator,
604    /// };
605    ///
606    /// /// Counts how many nodes a filter's expression tree contains.
607    /// struct NodeCounter;
608    ///
609    /// impl<'a> ExprVisitor<'a, usize> for NodeCounter {
610    ///     fn visit_literal(&mut self, _value: &FilterValue) -> usize { 1 }
611    ///     fn visit_property(&mut self, _name: &str) -> usize { 1 }
612    ///     fn visit_function_call(&mut self, _function: &dyn Function, args: &[Expr]) -> usize {
613    ///         1 + args.iter().map(|arg| self.visit_expr(arg)).sum::<usize>()
614    ///     }
615    ///     fn visit_binary(&mut self, l: &'a Expr<'a>, _op: BinaryOperator, r: &'a Expr<'a>) -> usize {
616    ///         1 + self.visit_expr(l) + self.visit_expr(r)
617    ///     }
618    ///     fn visit_logical(&mut self, l: &'a Expr<'a>, _op: LogicalOperator, r: &'a Expr<'a>) -> usize {
619    ///         1 + self.visit_expr(l) + self.visit_expr(r)
620    ///     }
621    ///     fn visit_unary(&mut self, _op: UnaryOperator, r: &'a Expr<'a>) -> usize {
622    ///         1 + self.visit_expr(r)
623    ///     }
624    ///     fn visit_like(&mut self, l: &'a Expr<'a>, _glob: &Glob) -> usize {
625    ///         1 + self.visit_expr(l)
626    ///     }
627    ///     # #[cfg(feature = "regex")]
628    ///     fn visit_matches(&mut self, l: &'a Expr<'a>, _re: &filt_rs::CompiledRegex) -> usize {
629    ///         1 + self.visit_expr(l)
630    ///     }
631    /// }
632    ///
633    /// # fn main() -> Result<(), filt_rs::Error> {
634    /// let filter = Filter::new("repo.public && repo.stars >= 50")?;
635    /// // (&&) + property + (>=) + property + literal → 5 nodes.
636    /// assert_eq!(filter.visit(&mut NodeCounter), 5);
637    /// # Ok(())
638    /// # }
639    /// ```
640    #[cfg(feature = "visitor")]
641    pub fn visit<'this, V, T>(&'this self, visitor: &mut V) -> T
642    where
643        V: ExprVisitor<'this, T>,
644    {
645        // The AST borrows from the pinned filter string, so its `'static`
646        // lifetime is really tied to `self`. Handing the visitor `&'this`
647        // borrows narrows that back down to the lifetime of this call.
648        visitor.visit_expr(&self.ast)
649    }
650
651    /// Internal expression walker used by [`Filter::matches`]. This is the same
652    /// as the public [`Filter::visit`], but remains available (crate-private)
653    /// when the `visitor` feature is disabled so that evaluation still works.
654    #[cfg(not(feature = "visitor"))]
655    pub(crate) fn visit<'this, V, T>(&'this self, visitor: &mut V) -> T
656    where
657        V: ExprVisitor<'this, T>,
658    {
659        visitor.visit_expr(&self.ast)
660    }
661
662    /// Gets the raw filter expression which was used to construct this filter.
663    ///
664    /// ```
665    /// use filt_rs::Filter;
666    ///
667    /// let filter = Filter::new("name == \"demo\"").unwrap();
668    /// assert_eq!(filter.raw(), "name == \"demo\"");
669    /// ```
670    pub fn raw(&self) -> &str {
671        &self.filter
672    }
673}
674
675impl Default for Filter {
676    /// Returns the match-everything filter `true`.
677    fn default() -> Self {
678        Self {
679            filter: Box::pin("true".to_string()),
680            ast: Expr::Literal(FilterValue::Bool(true)),
681            functions: base_functions(),
682        }
683    }
684}
685
686impl Clone for Filter {
687    fn clone(&self) -> Self {
688        // Re-parse against the same function set so that any custom functions
689        // registered with [`Filter::with_functions`] remain available.
690        Self::build(self.raw().to_string(), Arc::clone(&self.functions)).expect("clone filter")
691    }
692}
693
694impl PartialEq for Filter {
695    fn eq(&self, other: &Self) -> bool {
696        self.ast == other.ast
697    }
698}
699
700impl std::fmt::Debug for Filter {
701    /// Formats the filter as its parsed expression tree, which can be useful
702    /// when debugging operator precedence issues.
703    ///
704    /// ```
705    /// use filt_rs::Filter;
706    ///
707    /// let filter = Filter::new("a || b && c").unwrap();
708    /// assert_eq!(format!("{filter:?}"), "(|| (property a) (&& (property b) (property c)))");
709    /// ```
710    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
711        write!(f, "{:?}", self.ast)
712    }
713}
714
715impl Display for Filter {
716    /// Formats the filter as its original raw expression.
717    ///
718    /// ```
719    /// use filt_rs::Filter;
720    ///
721    /// let filter = Filter::new("a || b").unwrap();
722    /// assert_eq!(filter.to_string(), "a || b");
723    /// ```
724    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
725        write!(f, "{}", self.raw())
726    }
727}
728
729#[cfg(feature = "serde")]
730impl serde::Serialize for Filter {
731    /// Serializes a `Filter` as its raw expression string.
732    ///
733    /// ```
734    /// use filt_rs::Filter;
735    ///
736    /// let filter = Filter::new("a || b").unwrap();
737    /// let json = serde_json::to_string(&filter).unwrap();
738    /// assert_eq!(json, r#""a || b""#);
739    /// ```
740    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
741    where
742        S: serde::Serializer,
743    {
744        serializer.serialize_str(self.raw())
745    }
746}
747
748#[cfg(feature = "serde")]
749impl<'de> serde::Deserialize<'de> for Filter {
750    /// Deserializes a `Filter` from a string containing a filter expression.
751    ///
752    /// Missing or `null` values are deserialized as the match-everything
753    /// filter `true`, making it easy to use optional filter fields within
754    /// your configuration structures.
755    ///
756    /// ```
757    /// use filt_rs::Filter;
758    ///
759    /// #[derive(serde::Deserialize)]
760    /// struct Config {
761    ///     #[serde(default)]
762    ///     filter: Filter,
763    /// }
764    ///
765    /// let config: Config = serde_json::from_str(r#"{"filter": "!repo.fork"}"#).unwrap();
766    /// assert_eq!(config.filter.raw(), "!repo.fork");
767    ///
768    /// let config: Config = serde_json::from_str("{}").unwrap();
769    /// assert_eq!(config.filter.raw(), "true");
770    /// ```
771    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
772    where
773        D: serde::Deserializer<'de>,
774    {
775        struct FilterVisitor;
776
777        impl<'de> serde::de::Visitor<'de> for FilterVisitor {
778            type Value = Filter;
779
780            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
781                formatter.write_str("a valid filter expression")
782            }
783
784            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
785            where
786                E: serde::de::Error,
787            {
788                Filter::new(v).map_err(serde::de::Error::custom)
789            }
790
791            fn visit_none<E>(self) -> Result<Self::Value, E>
792            where
793                E: serde::de::Error,
794            {
795                Filter::new("true").map_err(serde::de::Error::custom)
796            }
797
798            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
799            where
800                D: serde::Deserializer<'de>,
801            {
802                deserializer.deserialize_str(self)
803            }
804        }
805
806        deserializer.deserialize_option(FilterVisitor)
807    }
808}
809
810#[cfg(test)]
811mod tests {
812    use rstest::rstest;
813
814    use super::*;
815
816    struct TestObject {
817        name: String,
818        age: i32,
819        alive: bool,
820        tags: Vec<&'static str>,
821    }
822
823    impl Default for TestObject {
824        fn default() -> Self {
825            Self {
826                name: "John Doe".to_string(),
827                age: 30,
828                alive: true,
829                tags: vec!["red", "black"],
830            }
831        }
832    }
833
834    impl Filterable for TestObject {
835        fn get(&self, property: &str) -> FilterValue<'_> {
836            match property {
837                "name" => self.name.clone().into(),
838                "age" => self.age.into(),
839                "alive" => self.alive.into(),
840                "tags" => self
841                    .tags
842                    .iter()
843                    .cloned()
844                    .map(|v| v.into())
845                    .collect::<Vec<FilterValue<'_>>>()
846                    .into(),
847                _ => FilterValue::Null,
848            }
849        }
850    }
851
852    #[rstest]
853    #[case("name == \"John Doe\"", true)]
854    #[case("name != \"John Doe\"", false)]
855    #[case("name == \"Jane Doe\"", false)]
856    #[case("name != \"Jane Doe\"", true)]
857    #[case("name startswith \"John\"", true)]
858    #[case("name startswith \"Jane\"", false)]
859    #[case("name endswith \"Doe\"", true)]
860    #[case("name endswith \"Smith\"", false)]
861    #[case("age == 30", true)]
862    #[case("age != 30", false)]
863    #[case("age == 31", false)]
864    #[case("age != 31", true)]
865    #[case("age > 31", false)]
866    #[case("age < 31", true)]
867    #[case("age >= 30", true)]
868    #[case("age <= 30", true)]
869    #[case("tags == [\"red\",\"black\"]", true)]
870    #[case("tags != [\"red\",\"black\"]", false)]
871    #[case("tags == [\"blue\"]", false)]
872    #[case("tags contains \"red\"", true)]
873    #[case("tags contains \"blue\"", false)]
874    #[case("\"red\" in tags", true)]
875    #[case("\"blue\" in tags", false)]
876    fn case_sensitive_filtering(#[case] filter: &str, #[case] matches: bool) {
877        let obj = TestObject::default();
878
879        assert_eq!(
880            Filter::new(filter)
881                .expect("parse filter")
882                .matches(&obj)
883                .expect("run filter"),
884            matches
885        );
886    }
887
888    #[rstest]
889    #[case("name == \"john doe\"", true)]
890    #[case("name != \"john doe\"", false)]
891    #[case("name == \"jane doe\"", false)]
892    #[case("name != \"jane doe\"", true)]
893    #[case("name startswith \"john\"", true)]
894    #[case("name startswith \"jane\"", false)]
895    #[case("name endswith \"doe\"", true)]
896    #[case("name endswith \"smith\"", false)]
897    #[case("\"RED\" in tags", true)]
898    #[case("\"BLUE\" in tags", false)]
899    fn case_insensitive_filtering(#[case] filter: &str, #[case] matches: bool) {
900        let obj = TestObject::default();
901
902        assert_eq!(
903            Filter::new(filter)
904                .expect("parse filter")
905                .matches(&obj)
906                .expect("run filter"),
907            matches
908        );
909    }
910
911    #[rstest]
912    #[case("name == \"John Doe\" && age == 30", true)]
913    #[case("name == \"John Doe\" && age == 31", false)]
914    #[case("name == \"Jane Doe\" && age == 30", false)]
915    #[case("name == \"John Doe\" || age == 30", true)]
916    #[case("name == \"John Doe\" || age == 31", true)]
917    #[case("name == \"Jane Doe\" || age == 30", true)]
918    #[case("name == \"Jane Doe\" || age == 31", false)]
919    fn binary_operator_filtering(#[case] filter: &str, #[case] matches: bool) {
920        let obj = TestObject::default();
921
922        assert_eq!(
923            Filter::new(filter)
924                .expect("parse filter")
925                .matches(&obj)
926                .expect("run filter"),
927            matches
928        );
929    }
930
931    #[rstest]
932    #[case("alive", true)]
933    #[case("!alive", false)]
934    #[case("name && age", true)]
935    #[case("name && !age", false)]
936    fn logical_operator_filtering(#[case] filter: &str, #[case] matches: bool) {
937        let obj = TestObject::default();
938
939        assert_eq!(
940            Filter::new(filter)
941                .expect("parse filter")
942                .matches(&obj)
943                .expect("run filter"),
944            matches
945        );
946    }
947
948    #[test]
949    fn default_filter_matches_everything() {
950        let filter = Filter::default();
951        assert_eq!(filter.raw(), "true");
952        assert!(filter.matches(&TestObject::default()).expect("run filter"));
953    }
954
955    #[test]
956    fn display_round_trips_the_raw_expression() {
957        let filter = Filter::new("age >= 30 && alive").expect("parse filter");
958        assert_eq!(filter.to_string(), "age >= 30 && alive");
959        assert_eq!(filter.raw(), "age >= 30 && alive");
960    }
961
962    #[test]
963    fn clone_preserves_the_raw_expression_and_behaviour() {
964        let filter = Filter::new("age >= 30 && alive").expect("parse filter");
965        let clone = filter.clone();
966
967        assert_eq!(clone.raw(), filter.raw());
968        assert_eq!(clone, filter);
969        assert_eq!(
970            clone.matches(&TestObject::default()).expect("run filter"),
971            filter.matches(&TestObject::default()).expect("run filter"),
972        );
973    }
974
975    #[test]
976    fn equal_filters_compare_equal() {
977        let lhs = Filter::new("age >= 30 && alive").expect("parse filter");
978        let rhs = Filter::new("age >= 30 && alive").expect("parse filter");
979        assert_eq!(lhs, rhs);
980    }
981
982    #[test]
983    fn different_filters_compare_unequal() {
984        let lhs = Filter::new("age >= 30").expect("parse filter");
985        let rhs = Filter::new("age >= 31").expect("parse filter");
986        assert_ne!(lhs, rhs);
987    }
988
989    #[rstest]
990    #[case("age >")]
991    #[case("(alive")]
992    #[case("name = \"John\"")]
993    #[case("\"unterminated")]
994    fn invalid_filters_report_errors(#[case] filter: &str) {
995        assert!(Filter::new(filter).is_err());
996    }
997
998    #[cfg(feature = "serde")]
999    mod serde_tests {
1000        use super::*;
1001
1002        #[derive(serde::Serialize, serde::Deserialize)]
1003        struct Config {
1004            #[serde(default)]
1005            filter: Filter,
1006        }
1007
1008        #[test]
1009        fn deserializes_a_filter_expression() {
1010            let config: Config =
1011                serde_json::from_str(r#"{"filter": "age > 21 && alive"}"#).expect("deserialize");
1012            assert_eq!(config.filter.raw(), "age > 21 && alive");
1013            assert!(
1014                config
1015                    .filter
1016                    .matches(&TestObject::default())
1017                    .expect("run filter")
1018            );
1019        }
1020
1021        #[test]
1022        fn missing_filters_match_everything() {
1023            let config: Config = serde_json::from_str("{}").expect("deserialize");
1024            assert_eq!(config.filter.raw(), "true");
1025        }
1026
1027        #[test]
1028        fn null_filters_match_everything() {
1029            let config: Config = serde_json::from_str(r#"{"filter": null}"#).expect("deserialize");
1030            assert_eq!(config.filter.raw(), "true");
1031        }
1032
1033        #[test]
1034        fn invalid_filters_fail_to_deserialize() {
1035            let result: Result<Config, _> = serde_json::from_str(r#"{"filter": "age >"}"#);
1036            assert!(result.is_err());
1037        }
1038
1039        #[test]
1040        fn serializes_a_filter_as_its_raw_expression() {
1041            let filter = Filter::new("age > 21 && alive").expect("parse filter");
1042            let json = serde_json::to_string(&filter).expect("serialize");
1043            assert_eq!(json, r#""age > 21 && alive""#);
1044        }
1045
1046        #[test]
1047        fn serializes_a_filter_field() {
1048            let config = Config {
1049                filter: Filter::new("!repo.fork").expect("parse filter"),
1050            };
1051            let json = serde_json::to_string(&config).expect("serialize");
1052            assert_eq!(json, r#"{"filter":"!repo.fork"}"#);
1053        }
1054
1055        #[test]
1056        fn round_trips_through_serde() {
1057            let original: Config =
1058                serde_json::from_str(r#"{"filter": "age > 21 && alive"}"#).expect("deserialize");
1059            let json = serde_json::to_string(&original).expect("serialize");
1060            let restored: Config = serde_json::from_str(&json).expect("deserialize");
1061            assert_eq!(restored.filter.raw(), original.filter.raw());
1062        }
1063    }
1064}