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
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 errorEvery 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 errorThe 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 errorjq_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 errorfilter 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/jq.qwer$ curl -O https://quillolang.org/plugins/files/jq.qweThen, in your program, all of it or only what you call:
use jq.qweruse jq_run, jq_first from jq // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
jaq-core = "=2.2.1"jaq-std = "=2.1.2"jaq-json = { version = "=1.1.3", features = ["serde_json"] }Example
// 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 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}Related
jsonpath.qwer · 1.0.0Query values with JSONPath (RFC 9535): filters, slices, recursive descent and functions, with the paths of what matched.8 views · 7 downloadsJSON types json_types.qwer · 1.0.0Types inferred from JSON samples: TypeScript interfaces, a JSON Schema and Rust structs for an API's answers or a webhook's body, with optional and nullable fields found across samples.7 views · 7 downloadsOpenAPI openapi.qwer · 1.0.0OpenAPI 3 documents read from JSON or YAML: each operation's path, parameters, body and responses, and a request path matched to its operation.8 views · 9 downloadsFHIR fhir.qwer · 1.0.0HL7 FHIR R4 JSON: resources checked for required fields, bound codes and date formats, Bundles read and built, references resolved across a Bundle, FHIRPath-lite field access, and search parameters with paging, sorting and includes for a FHIR server.3 views · 7 downloadsJSON Patch json_patch.qwer · 1.0.0JSON Patch (RFC 6902) and Merge Patch (RFC 7396): the difference between two values as operations, and patches applied.10 views · 6 downloadsJSON Schema json_schema.qwer · 1.0.0Check values against a JSON Schema (drafts 4 to 2020-12), with every error and the path of the value at fault.8 views · 8 downloadsThread 0 posts
On jq: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.