Developer tools

jq

jq filters over JSON with jaq: pick, reshape, filter and sum API answers, webhook bodies and JSON lines in the language people already write in jq, with variables and a check for typed-in filters.

Version 1.0.0 · 5 functions · 3 crates

#json#query#api#reports

Functions

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

jq_run(filter: str, input: any) → listor an error

Every value filter gives for input, as a list: jq_run(".items[] | select(.qty > 1) | .sku", order). jq's language as jaq has it: paths, pipes, select, map, group_by, reduce, to_entries, string interpolation, @csv, @base64... An error value says where a filter does not read, or what failed while it ran.

jq_first(filter: str, input: any) → anyor an error

The first value filter gives for input, or null when it gives none: for a filter that makes one answer.

jq_with(filter: str, input: any, vars: map) → listor an error

jq_run with variables: each key of vars is $key in the filter, as jq --argjson key value gives: jq_with(".[] | select(.price < $max)", items, { max: 20 }). Keeps filters free of values pasted into their text.

jq_stream(filter: str, text: str, slurp: bool) → listor an error

filter run over each JSON value in text (JSON lines, or values one after another, as jq reads a stream): every value given, in order. With slurp true the values are put in one list first, as jq -s does.

jq_check(filter: str) → str?

Whether filter reads and every name in it is defined: null when it does, else the error, for filters people type in a form.

Get it

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

Or with curl:

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

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

main.qwe
use jq.qweruse jq_run, jq_first from jq      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
jaq-core = "=2.2.1"jaq-std = "=2.1.2"jaq-json = { version = "=1.1.3", features = ["serde_json"] }

Example

jq.qwe
// jq filters over an order API's answer, and over a day of JSON-lines events, as an admin// might save them for reports.////   quillo run jq.qwe      then open http://localhost:8080/reportuse "jq.qwer"let ORDERS = {    orders: [        { id: 1042, customer: { name: "Ada", country: "NL" }, items: [{ sku: "MUG", qty: 2, price: 9.5 }, { sku: "TEA", qty: 1, price: 6.0 }], status: "paid" },        { id: 1043, customer: { name: "Bas", country: "BE" }, items: [{ sku: "MUG", qty: 1, price: 9.5 }], status: "refunded" },        { id: 1044, customer: { name: "Cleo", country: "NL" }, items: [{ sku: "POT", qty: 1, price: 32.0 }, { sku: "TEA", qty: 3, price: 6.0 }], status: "paid" },    ],}route GET /report = {    paid_totals: jq_run(`.orders[] | select(.status == "paid") | { id, total: ([.items[] | .qty * .price] | add) }`, ORDERS)?,    by_country: jq_first(`[.orders[] | { c: .customer.country, n: 1 }] | group_by(.c) | map({ key: .[0].c, value: length }) | from_entries`, ORDERS)?,    skus: jq_first(`[.orders[].items[].sku] | unique | join(", ")`, ORDERS)?,    csv: jq_run(`.orders[] | [.id, .customer.name, .status] | @csv`, ORDERS)?,    under: jq_with(`[.orders[] | select(([.items[] | .qty * .price] | add) < $max) | .id]`, ORDERS, { max: 25 })?,}let EVENTS = `{"at":"09:00","type":"signup","user":"u1"}{"at":"09:05","type":"purchase","user":"u1","amount":19.0}{"at":"09:30","type":"purchase","user":"u2","amount":42.5}{"at":"10:10","type":"signup","user":"u3"}`route GET /events = {    purchases: jq_stream(`select(.type == "purchase") | .amount`, EVENTS, false)?,    revenue: jq_stream(`map(.amount // 0) | add`, EVENTS, true)?[0],}route GET /check = {    fine: jq_check(`.orders | map(.id)`),    typo: jq_check(`.orders | map(.id`),    unknown: jq_check(`.orders | mapp(.id)`),    runtime: jq_run(`.orders[0].id | ascii_downcase`, ORDERS).error,}
$ quillo run jq.qwe$ curl 'localhost:8080/report'$ curl 'localhost:8080/events'$ curl 'localhost:8080/check'

Source

jq.qwer
//! jq filters over JSON, with jaq: pick, reshape, filter and sum API answers, webhook bodies and JSON lines with the language people already write in jq.//! version: 1.0.0//!//! [dependencies]//! jaq-core = "=2.2.1"//! jaq-std = "=2.1.2"//! jaq-json = { version = "=1.1.3", features = ["serde_json"] }use jaq_core::load::{Arena, File, Loader};use jaq_core::{Compiler, Ctx, RcIter};use jaq_json::Val;use serde_json::Value;const MAX_OUT: usize = 100_000;fn position(code: &str, rest: &str) -> String {    let at = code.len().saturating_sub(rest.len());    let col = code[..at].chars().count() + 1;    let near: String = rest.chars().take(12).collect();    if near.is_empty() { format!("at the end (column {})", col) } else { format!("at column {} (`{}`)", col, near) }}/// Runs `filter` over each of `inputs` with `$name` variables from `vars`, at most MAX_OUT values in all.fn run(filter: &str, inputs: Vec<Value>, vars: serde_json::Map<String, Value>) -> Result<Vec<Value>, String> {    let program = File { code: filter, path: () };    let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs()));    let arena = Arena::default();    let modules = loader.load(&arena, program).map_err(|errs| {        let mut msgs = vec![];        for (_, e) in errs {            match e {                jaq_core::load::Error::Io(v) => msgs.extend(v.into_iter().map(|(p, m)| format!("{}: {}", p, m))),                jaq_core::load::Error::Lex(v) => msgs.extend(v.into_iter().map(|(exp, rest)| format!("expected a {} {}", exp.as_str(), position(filter, rest)))),                jaq_core::load::Error::Parse(v) => msgs.extend(v.into_iter().map(|(exp, rest)| format!("expected {} {}", exp.as_str(), position(filter, rest)))),            }        }        format!("the filter does not read: {}", msgs.join("; "))    })?;    let names: Vec<String> = vars.keys().map(|k| format!("${}", k.trim_start_matches('$'))).collect();    let compiled = Compiler::default()        .with_funs(jaq_std::funs().chain(jaq_json::funs()))        .with_global_vars(names.iter().map(|s| s.as_str()))        .compile(modules)        .map_err(|errs| {            let msgs: Vec<String> = errs                .into_iter()                .flat_map(|(_, es)| {                    es.into_iter().map(|(name, what)| match what {                        jaq_core::compile::Undefined::Filter(n) => format!("no filter `{}` taking {} argument{}", name, n, if n == 1 { "" } else { "s" }),                        jaq_core::compile::Undefined::Var => format!("no variable `{}`", name),                        other => format!("`{}` is not defined ({:?})", name, other),                    })                })                .collect();            msgs.join("; ")        })?;    let mut out = vec![];    let empty = RcIter::new(core::iter::empty());    for input in inputs {        let ctx = Ctx::new(vars.values().cloned().map(Val::from), &empty);        for r in compiled.run((ctx, Val::from(input))) {            match r {                Ok(v) => out.push(Value::from(v)),                Err(e) => return Err(format!("the filter failed: {}", e)),            }            if out.len() > MAX_OUT {                return Err(format!("the filter gave more than {} values", MAX_OUT));            }        }    }    Ok(out)}/// Every value `filter` gives for `input`, as a list: `jq_run(".items[] | select(.qty > 1) | .sku", order)`. jq's language as/// jaq has it: paths, pipes, `select`, `map`, `group_by`, `reduce`, `to_entries`, string interpolation, `@csv`, `@base64`.../// An error value says where a filter does not read, or what failed while it ran.pub fn jq_run(filter: &str, input: serde_json::Value) -> Result<Vec<Value>, String> {    run(filter, vec![input], serde_json::Map::new())}/// The first value `filter` gives for `input`, or null when it gives none: for a filter that makes one answer.pub fn jq_first(filter: &str, input: serde_json::Value) -> Result<Value, String> {    Ok(run(filter, vec![input], serde_json::Map::new())?.into_iter().next().unwrap_or(Value::Null))}/// `jq_run` with variables: each key of `vars` is `$key` in the filter, as `jq --argjson key value` gives:/// `jq_with(".[] | select(.price < $max)", items, { max: 20 })`. Keeps filters free of values pasted into their text.pub fn jq_with(filter: &str, input: serde_json::Value, vars: std::collections::HashMap<String, serde_json::Value>) -> Result<Vec<Value>, String> {    let mut keys: Vec<_> = vars.into_iter().collect();    keys.sort_by(|a, b| a.0.cmp(&b.0));    run(filter, vec![input], keys.into_iter().collect())}/// `filter` run over each JSON value in `text` (JSON lines, or values one after another, as `jq` reads a stream): every value given,/// in order. With `slurp` true the values are put in one list first, as `jq -s` does.pub fn jq_stream(filter: &str, text: &str, slurp: bool) -> Result<Vec<Value>, String> {    let mut inputs = vec![];    for (i, v) in serde_json::Deserializer::from_str(text).into_iter::<Value>().enumerate() {        inputs.push(v.map_err(|e| format!("value {} is not JSON: {}", i + 1, e))?);    }    if slurp {        inputs = vec![Value::Array(inputs)];    }    run(filter, inputs, serde_json::Map::new())}/// Whether `filter` reads and every name in it is defined: null when it does, else the error, for filters people type in a form.pub fn jq_check(filter: &str) -> Option<String> {    let names = jq_free_vars(filter);    let vars: serde_json::Map<String, Value> = names.into_iter().map(|n| (n, Value::Null)).collect();    match run(filter, vec![], vars) {        Ok(_) => None,        Err(e) => Some(e),    }}/// `$names` used in a filter (outside strings), so a checked filter may use variables given later.fn jq_free_vars(filter: &str) -> Vec<String> {    let mut out = vec![];    let mut in_str = false;    let mut chars = filter.char_indices().peekable();    while let Some((_, c)) = chars.next() {        match c {            '\\' if in_str => {                chars.next();            }            '"' => in_str = !in_str,            '$' if !in_str => {                let mut name = String::new();                while let Some(&(_, n)) = chars.peek() {                    if n.is_alphanumeric() || n == '_' {                        name.push(n);                        chars.next();                    } else {                        break;                    }                }                if !name.is_empty() && name != "__loc__" && !out.contains(&name) && !filter.contains(&format!("as ${}", name)) {                    out.push(name);                }            }            _ => {}        }    }    out}
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.