Developer tools

JsonLogic rules

Business rules as JsonLogic, kept as JSON and shared with the browser: evaluated against data, used as filters and as decision tables.

Version 1.0.0 · 4 functions · 1 crate

#rules#pricing#flags#json

Functions

What use json_logic.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use jsonlogic_apply, jsonlogic_test from json_logic (Quillo 0.6).

jsonlogic_apply(rule: any, data: any) → anyor an error

The value of a rule for data: { "if": [{ ">": [{ "var": "cart.total" }, 100] }, "free", "paid"] } with { cart: { total: 140 } } is "free". Operators: var (dots go into maps, missing), == != < <= > >=, and or ! !!, if, + - * / % min max, in, cat, substr, map filter reduce all some none merge.

jsonlogic_test(rule: any, data: any) → boolor an error

Whether a rule holds for data, by JsonLogic's truthiness (false, null, 0, "" and [] are false).

jsonlogic_filter(rule: any, items: list) → listor an error

The items a rule holds for, in order: a saved search, a segment of customers, offers a user qualifies for. At most 100,000 items.

jsonlogic_decide(rules: list, data: any, otherwise: any) → anyor an error

A decision table: rules are [{ when: <rule>, then: <value> }], tried in order; gives { then, index } of the first whose when holds for data, or { then: otherwise, index: -1 } when none does. then may itself be a rule, evaluated with the same data.

Get it

$ quillo install json_logic               # json_logic.qwer into this folder$ quillo install json_logic --example     # and its example, json_logic.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/json_logic.qwer$ curl -O https://quillolang.org/plugins/files/json_logic.qwe

Then, in your program, all of it or only what you call:

main.qwe
use json_logic.qweruse jsonlogic_apply, jsonlogic_test from json_logic      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
jsonlogic-rs = "0.5"

Example

json_logic.qwe
// Shipping tiers and promotions as JsonLogic rules, as an admin would keep them in the// database and the checkout page would also run them in the browser (json-logic-js).////   quillo run json_logic.qwe      then open http://localhost:8080/shipping?total=80&country=NLuse "json_logic.qwer"let tiers = [    { when: { ">=": [{ var: "total" }, 100] }, then: "free" },    { when: { and: [{ in: [{ var: "country" }, ["NL", "BE", "LU"]] }, { ">=": [{ var: "total" }, 50] }] }, then: "benelux" },    { when: { "==": [{ var: "member" }, true] }, then: "member" },]route GET /shipping ? { total: float, country: str = "DE", member: bool = false } {    let d = jsonlogic_decide(tiers, { total, country, member }, "standard")?    { tier: d.then, rule: d.index }}let customers = [    { name: "Ada", orders: 14, country: "UK", newsletter: true },    { name: "Bas", orders: 2, country: "NL", newsletter: true },    { name: "Cleo", orders: 31, country: "NL", newsletter: false },]route GET /segment = jsonlogic_filter({ and: [{ ">": [{ var: "orders" }, 10] }, { "!": [{ "==": [{ var: "country" }, "UK"] }] }] }, customers)? | map((c) => c.name)route GET /price = {    discount: jsonlogic_apply({ if: [{ ">=": [{ var: "qty" }, 10] }, { "*": [{ var: "price" }, 0.1] }, 0] }, { qty: 12, price: 30 })?,    eligible: jsonlogic_test({ "<=": [18, { var: "age" }, 65] }, { age: 40 })?,    missing: jsonlogic_apply({ missing: ["email", "name"] }, { name: "Ada" })?,}
$ quillo run json_logic.qwe$ curl 'localhost:8080/shipping?total=80&country=NL'$ curl 'localhost:8080/segment'$ curl 'localhost:8080/price'

Source

json_logic.qwer
//! Business rules as JsonLogic: rules kept as JSON (in a database, edited in an admin, shared with a JavaScript front end) and evaluated against data, for eligibility, pricing tiers, feature flags and form logic.//! version: 1.0.0//!//! [dependencies]//! jsonlogic-rs = "0.5"use serde_json::{json, Value};const MAX_RULE: usize = 256 * 1024;/// JsonLogic's own truthiness, as in JavaScript: false, null, 0, "" and [] are false.fn truthy(v: &Value) -> bool {    match v {        Value::Null => false,        Value::Bool(b) => *b,        Value::Number(n) => n.as_f64().map(|f| f != 0.0 && !f.is_nan()).unwrap_or(false),        Value::String(s) => !s.is_empty(),        Value::Array(a) => !a.is_empty(),        Value::Object(_) => true,    }}fn apply(rule: &Value, data: &Value) -> Result<Value, String> {    if rule.to_string().len() > MAX_RULE {        return Err("a rule is at most 256 KB of JSON".into());    }    jsonlogic_rs::apply(rule, data).map_err(|e| e.to_string())}/// The value of a rule for `data`: `{ "if": [{ ">": [{ "var": "cart.total" }, 100] }, "free", "paid"] }` with `{ cart: { total: 140 } }` is "free"./// Operators: `var` (dots go into maps, `missing`), `== != < <= > >=`, `and or ! !!`, `if`, `+ - * / % min max`, `in`, `cat`, `substr`, `map filter reduce all some none merge`.pub fn jsonlogic_apply(rule: serde_json::Value, data: serde_json::Value) -> Result<Value, String> {    apply(&rule, &data)}/// Whether a rule holds for `data`, by JsonLogic's truthiness (false, null, 0, "" and [] are false).pub fn jsonlogic_test(rule: serde_json::Value, data: serde_json::Value) -> Result<bool, String> {    apply(&rule, &data).map(|v| truthy(&v))}/// The items a rule holds for, in order: a saved search, a segment of customers, offers a user qualifies for. At most 100,000 items.pub fn jsonlogic_filter(rule: serde_json::Value, items: Vec<serde_json::Value>) -> Result<Vec<Value>, String> {    if items.len() > 100_000 {        return Err("at most 100,000 items at once".into());    }    let mut out = Vec::new();    for it in items {        if truthy(&apply(&rule, &it)?) {            out.push(it);        }    }    Ok(out)}/// A decision table: `rules` are `[{ when: <rule>, then: <value> }]`, tried in order; gives `{ then, index }` of the first whose `when` holds for `data`,/// or `{ then: otherwise, index: -1 }` when none does. `then` may itself be a rule, evaluated with the same data.pub fn jsonlogic_decide(rules: Vec<serde_json::Value>, data: serde_json::Value, otherwise: serde_json::Value) -> Result<Value, String> {    for (i, r) in rules.iter().enumerate() {        let when = r.get("when").ok_or(format!("rule {} has no `when`", i))?;        if truthy(&apply(when, &data)?) {            let then = r.get("then").cloned().unwrap_or(Value::Null);            let v = if then.is_object() { apply(&then, &data)? } else { then };            return Ok(json!({ "then": v, "index": i }));        }    }    Ok(json!({ "then": otherwise, "index": -1 }))}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

On JsonLogic rules: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.