tinyfilter is a extremely tiny filtering language.
It's like expr-lang but safe for untrusted human input.
It was built for backbeat's table folder language.
Add it to your repo with:
cargo add tinyfilterand for javascript
npm add tinyfilter
# or whatever package manager the kids use nowadaysI need a tiny, easily embeddable filter language for backbeat that is safe against untrusted user input.
It also needs a tiny surface area, as the spec for this is technically part of the backbeat spec.
It's unsafe to evaluate untrusted expr-lang inputs. For example, evaluating 1..9007199254740992 will let you instantly crash the host, and expr-lang has far too much of an implementation surface for something I want to be portable.
In Rust:
fn main() {
let mut cx = tinyfilter::Context::default();
cx.insert("age", 24.0);
cx.insert("name", "zk");
cx.insert("hasID", false);
match tinyfilter::evaluate("age >= 18 and hasID", &cx) {
Ok(true) => println!("filter matched"),
Ok(false) => println!("filter didn't match"),
Err(err) => println!("the user's filter was invalid: {err}"),
}
}In Typescript:
import { evaluate } from "tinyfilter";
let result = evaluate("age >= 18 and hasID", {
age: 24,
name: "zk",
hasID: false,
});Filters generally look like this:
hasID and age > 18
A filter must resolve to either true or false. An expression that doesn't resolve to a boolean will error.
There is no let or any way to create intermediate bindings.
You can do conditionals with if/else, if you want to use some extra lines.
if age < 18 {
false
} else {
hasID
}
There are:
str, which is UTF8 text. You can write a string literal with"asdf".num, which is a 64 bit floating point number. You can define integers e.g.123or decimals123.456. You can also put_to separate integers up e.g.1_000_000.bool, which is true or false. You can writetrueorfalseto specify a boolean.map, which are key value pairs. You can index into a map withmap.valueormap["value with funny name"]. There is no map literal syntax.nil, which is produced from indexing into a map that doesn't have a value for that key.
There are no arrays/lists, and there is no syntax for a map literal. (by design, use expr-lang if you need powerful stuff)
Add two numbers together. There is no string concatenation.
5 + 5 == 101.7 + 5 == 6.7"a" + "b"(error)5 + true(error)
Subtract two numbers.
5 - 5 == 01.7 - 5 == -3.3
Multiply two numbers.
2 * 1.5 == 3
Divide two numbers. Use floor(a / b) for integer division.
5 / 2 == 2.55 / 0(error)
Return the remainder of dividing a by b.
10 % 3 == 110 % 3.1 == 0.7-5.5 % 2 == -1.5
Return a to the power of b.
5 ** 3 == 1255 ** 2.5 == 55.901699437494745 ** -1 == 1/5
Return whether a is equal to b. Errors if either side of argument is a map.
(5 == 5) == true(5 == 4) == false(5 == "5") == false("foo" == "foo") == true
Return whether a is not equal to b. Errors if either side of argument is a map
(5 != 4) == true(5 != 5) == false(5 != "5") == true
Return whether a is less than (<), less than or equal to (<=), greater than (>), greater than or equal to (>=) b.
5 > 4 == true5 > 5 == false5 >= 5 == true5.5 < 6 == true5.5 < 1 == false5.5 <= 5.5 == true
Return the boolean opposite of a.
not true == falsenot false == truenot 12(error)
Return true if a and b are true.
true and true == truetrue and false == false1 and true(error)
Return true if a or b are true.
true or false == truefalse or false == false1 or true(error)
Returns b if a is null.
tags.difficulty otherwise 0 == 0(assume thattags.difficultyis nil)tags.isAwesome otherwise false == true(assume thattags.isAwesomeis true)"a" otherwise "b"(error)
Returns true if a is not nil.
exists tags.difficulty == falseexists tags.isAwesome == truenot exists tags.difficulty == true
Takes a string and converts it into a number. Floats get truncated.
num("14") == 14num("14.123") == 14.123num("a")(error)num("1e10")(error)
Takes anything and returns a string indicating what type it is.
type(5) == "num"type(5.5) == "num"type("hi") == "str"type(nil) == "nil"type(true) == "bool"
Returns whether the first string starts with the second string.
startsWith("md5/70924d6fa4b2d745185fa4660703a5c0", "md5/") == truestartsWith("md5/70924d6fa4b2d745185fa4660703a5c0", "sha256/") == false
Returns whether the first string contains the second string.
contains("Blue Rain", "Rain") == truecontains("Blue Rain", "Black") == false
Returns whether the first string ends with the second string.
endsWith("epic0", "0") == trueendsWith("epic0", "1") == false
Return the absolute value of a number.
abs(-5.1) == 5.1
Returns the number rounded up.
ceil(5.1) == 6
Returns the number rounded down.
floor(5.9) == 5
Round the number to the nearest whole number.
round(5.1) == 5round(5.9) == 6round(5.5) == 6
Return the smallest of two numbers.
min(9, 5.5) == 5.5
Return the largest of two numbers.
max(9, 5.5) == 9