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}