Skip to main content

Crate filt_rs

Crate filt_rs 

Source
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

LiteralExampleNotes
NullnullAlso returned for properties which aren’t found.
Booleantrue, false
Number123, 123.45All numbers are 64-bit floats internally.
String"hello"Escape embedded quotes with \".
Raw stringr"^v\d+$"No escape processing; cannot contain " (the r#"..."# form is not supported).
Tuple["a", "b"]A list of literal values.
Duration5m, 1h30m, 500msRequires 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:

OperatorMeaning
||Logical OR (short-circuiting).
&&Logical AND (short-circuiting).
==, !=Equality (strings are compared case-insensitively).
>, >=, <, <=Ordering comparisons.
containsString contains a substring, or tuple contains a value.
inInverse of contains (i.e. a in bb contains a).
startswith, endswithString prefix/suffix tests (case-insensitive).
likeCase-insensitive glob match (* and ? wildcards).
matchesRegular 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.

FunctionResult
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), or w (weeks) — and may chain several segments together: 90s, 5m, 1h30m, 500ms.
  • Filterable::get implementations can return FilterValue::DateTime values (e.g. from chrono::DateTime<Utc> or std::time::SystemTime).
  • Datetimes and durations support ordering comparisons against values of the same type, and arithmetic via + and -: DateTime ± Duration → DateTime, DateTime - DateTime → Duration, and Duration ± 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() - 5m

Without 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 the matches regular expression operator (adds a dependency on the regex crate). Without this feature, filters using matches fail to parse with an error explaining how to enable it.

  • chrono — adds datetime and duration support: the FilterValue::DateTime and FilterValue::Duration variants, duration literals such as 5m and 1h30m, the now() function, and temporal arithmetic and comparisons (see Datetimes and durations).

  • secrecy — adds a FilterValue::Secret variant backed by the secrecy crate’s SecretString. Secret values behave exactly like strings in every comparison operation, but are always formatted as [REDACTED], making it impossible to leak them through logging. See FilterValue::secret for 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 — implements serde::Deserialize for Filter, allowing filters to be parsed directly out of configuration files (a missing or null value deserializes to the match-everything true filter).

  • visitor — exposes the parsed expression tree and a visitor interface: the Expr AST, the ExprVisitor trait, the BinaryOperator, LogicalOperator, and UnaryOperator enums, and the Filter::visit method. 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 the property_collector example (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 Filterable objects.

Enums§

FilterValue
A value which may appear within a filter expression, either as a literal or as the result of resolving a property on a Filterable object.

Traits§

Filterable
A trait for types which can be filtered by the filter system.