Expand description
A human-friendly filter expression language for matching your objects against user-provided queries.
This crate provides a small, dependency-light filtering DSL designed for
situations where your users need to describe which items a tool should
operate on — for example which repositories to back up, which emails to
restore, or which releases to download. It was originally developed for
(and extracted from) the Sierra Softworks
github-backup and
mail-backup projects.
§Quick start
Implement the Filterable trait on your type to expose the properties
which may be referenced in a filter expression, then parse a Filter
and evaluate it against your objects.
use filt_rs::{Filter, FilterValue, Filterable};
struct Repo {
name: &'static str,
public: bool,
stars: u32,
}
impl Filterable for Repo {
fn get(&self, key: &str) -> FilterValue<'_> {
match key {
"repo.name" => self.name.into(),
"repo.public" => self.public.into(),
"repo.stars" => self.stars.into(),
_ => FilterValue::Null,
}
}
}
let filter = Filter::new("repo.public && repo.stars >= 50")?;
let repo = Repo { name: "git-tool", public: true, stars: 87 };
assert!(filter.matches(&repo)?);
let repo = Repo { name: "top-secret", public: false, stars: 3 };
assert!(!filter.matches(&repo)?);§Filter syntax
A filter is a single logical expression which is evaluated against each object, matching the object whenever the expression is truthy.
repo.public && !repo.fork && repo.name in ["git-tool", "grey"]§Literals
| Literal | Example | Notes |
|---|---|---|
| Null | null | Also returned for properties which aren’t found. |
| Boolean | true, false | |
| Number | 123, 123.45 | All numbers are 64-bit floats internally. |
| String | "hello" | Escape embedded quotes with \". |
| Raw string | r"^v\d+$" | No escape processing; cannot contain " (the r#"..."# form is not supported). |
| Tuple | ["a", "b"] | A list of literal values. |
| Duration | 5m, 1h30m, 500ms | Requires the chrono crate feature. |
§Properties
Any other identifier (including . and - separated names like
release.prerelease or asset.source-code) is treated as a property
reference, and is resolved by calling Filterable::get on the target
object. Note that the operator keywords below (in, contains, like,
matches, etc.) are reserved and cannot be used as property names.
§Operators
In order of increasing precedence:
| Operator | Meaning |
|---|---|
|| | Logical OR (short-circuiting). |
&& | Logical AND (short-circuiting). |
==, != | Equality (strings are compared case-insensitively). |
>, >=, <, <= | Ordering comparisons. |
contains | String contains a substring, or tuple contains a value. |
in | Inverse of contains (i.e. a in b ≡ b contains a). |
startswith, endswith | String prefix/suffix tests (case-insensitive). |
like | Case-insensitive glob match (* and ? wildcards). |
matches | Regular expression match (requires the regex crate feature). |
+, - | Addition and subtraction (numbers, datetimes, and durations). |
! | Logical NOT (unary). |
(...) | Grouping. |
§Case sensitivity
The string operators above compare case-insensitively, folding both
operands with the language’s Unicode case-folding rules. Each of them
(except matches, where the pattern author controls casing with (?i))
has a case-sensitive variant with a _cs suffix which compares strings
exactly as written: contains_cs, in_cs, startswith_cs,
endswith_cs, and like_cs. They sit at the same precedence as their
case-insensitive counterparts, and tuple membership through contains_cs
and in_cs compares the tuple’s elements case-sensitively too.
branch.name startswith_cs "Feat/" && "Alice" in_cs branch.reviewers§Pattern matching
The like operator matches a string against a glob pattern. * matches
any sequence of characters (including none), ? matches exactly one
character, and a backslash makes the following character literal (\*,
\?, \\); character classes like [a-z] are not supported. Like
the rest of the language, matching is case-insensitive: both the pattern
and the input are folded using the language’s Unicode case-folding rules,
including multi-character folds ("groß" like "*ss" holds, and ?
counts folded characters, so ß counts as two). The like_cs variant
matches case-sensitively instead, with no folding at all.
use filt_rs::{Filter, FilterValue, Filterable};
struct Branch(&'static str);
impl Filterable for Branch {
fn get(&self, key: &str) -> FilterValue<'_> {
match key {
"branch.name" => self.0.into(),
_ => FilterValue::Null,
}
}
}
let filter = Filter::new(r#"branch.name like "feat/*""#)?;
assert!(filter.matches(&Branch("feat/login"))?);
assert!(filter.matches(&Branch("FEAT/LOGIN"))?);
assert!(!filter.matches(&Branch("fix/typo"))?);With the regex crate feature enabled, the matches operator tests a
string against a regular expression (as implemented by the
regex crate). Raw strings (r"...") are the most
convenient way to write these, since they perform no escape processing.
Unlike the rest of the language, regular expressions are case-sensitive as
written (use (?i) to ignore case) and unanchored (use ^ and $ to
anchor the match).
let filter = Filter::new(r#"branch.name matches r"^release/v\d+(\.\d+){2}$""#)?;
assert!(filter.matches(&Branch("release/v1.2.3"))?);
assert!(!filter.matches(&Branch("release/v1.2"))?);Both operators require their pattern to be a string literal: the pattern
is compiled once when the filter is parsed (with invalid regular
expressions reported as friendly Filter::new errors), and evaluation
performs no pattern-related heap allocation. Glob evaluation is fully
allocation-free, while regex evaluation is amortized allocation-free
(the regex engine lazily allocates per-thread scratch space on first use
and reuses it thereafter). Only string values can match a pattern: tuples
match when any of their string elements match, while null, booleans, and
numbers never match — even against like "*".
§Arithmetic
The + and - operators bind tighter than comparisons, so
a + b > c is read as (a + b) > c. Numbers may be added to and
subtracted from one another, while any unsupported combination of operand
types evaluates to null (consistent with the language’s lenient
comparison semantics). There is no unary minus: write 0 - 5 to produce a
negative value.
let filter = Filter::new("1 + 2 - 4 < 0")?;
assert!(filter.matches(&Nothing)?);Note that a - inside a property name remains part of that name (so
asset.source-code is a single property), while a - which starts a new
token is the subtraction operator: asset.size - 5 subtracts, but
asset.size-5 references a property named asset.size-5.
§Functions
Filters may call built-in functions using the familiar name(args...)
syntax. Function names and argument counts are validated when the filter
is parsed, so typos fail fast with a friendly error rather than at
evaluation time.
| Function | Result |
|---|---|
now() | The current UTC time, evaluated at each Filter::matches call. Requires chrono. |
§Datetimes and durations
With the chrono crate feature enabled, filters can work with points
in time and spans of time:
- Duration literals are written as a number immediately followed by a
unit —
ms(milliseconds),s(seconds),m(minutes),h(hours),d(days), orw(weeks) — and may chain several segments together:90s,5m,1h30m,500ms. Filterable::getimplementations can returnFilterValue::DateTimevalues (e.g. fromchrono::DateTime<Utc>orstd::time::SystemTime).- Datetimes and durations support ordering comparisons against values of
the same type, and arithmetic via
+and-:DateTime ± Duration → DateTime,DateTime - DateTime → Duration, andDuration ± Duration → Duration. - Datetimes are always truthy, while durations are truthy if (and only if) they are non-zero.
This makes relative-time filters pleasantly concise:
event.timestamp > now() - 5mWithout the chrono feature, duration literals and now() are still
recognised by the parser but produce a friendly error explaining that the
feature must be enabled.
§Crate features
-
regex— enables thematchesregular expression operator (adds a dependency on the regex crate). Without this feature, filters usingmatchesfail to parse with an error explaining how to enable it. -
chrono— adds datetime and duration support: theFilterValue::DateTimeandFilterValue::Durationvariants, duration literals such as5mand1h30m, thenow()function, and temporal arithmetic and comparisons (see Datetimes and durations). -
secrecy— adds aFilterValue::Secretvariant backed by thesecrecycrate’sSecretString. Secret values behave exactly like strings in every comparison operation, but are always formatted as[REDACTED], making it impossible to leak them through logging. SeeFilterValue::secretfor details.use filt_rs::{Filter, FilterValue, Filterable}; struct Credentials { password: secrecy::SecretString, } impl Filterable for Credentials { fn get(&self, key: &str) -> FilterValue<'_> { match key { "password" => self.password.clone().into(), _ => FilterValue::Null, } } } let creds = Credentials { password: "hunter2".into() }; // Secrets compare exactly like strings within filter expressions... let filter = Filter::new(r#"password == "Hunter2""#).unwrap(); assert!(filter.matches(&creds).unwrap()); // ...but they are always redacted when formatted. assert_eq!(creds.get("password").to_string(), "[REDACTED]"); -
serde— implementsserde::DeserializeforFilter, allowing filters to be parsed directly out of configuration files (a missing ornullvalue deserializes to the match-everythingtruefilter). -
visitor— exposes the parsed expression tree and a visitor interface: theExprAST, theExprVisitortrait, theBinaryOperator,LogicalOperator, andUnaryOperatorenums, and theFilter::visitmethod. This lets downstream crates walk and transform a filter — for example to collect the properties it references, estimate its cost, or translate it into another query language. See theproperty_collectorexample (cargo run --example property_collector --features visitor) for a worked illustration.
Structs§
- Error
- The fundamental error type used by this library.
- Filter
- A parsed filter expression which can be evaluated against
Filterableobjects.
Enums§
- Filter
Value - A value which may appear within a filter expression, either as a literal
or as the result of resolving a property on a
Filterableobject.
Traits§
- Filterable
- A trait for types which can be filtered by the filter system.