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