Skip to main content

api_testing_core/
jq.rs

1use std::collections::BTreeMap;
2
3use anyhow::Context;
4use jaq_core::load::{Arena, File, Loader};
5use jaq_core::{Ctx, Vars, data, unwrap_valr};
6use jaq_json::{Val, write};
7
8use crate::Result;
9
10fn to_jaq_val(value: &serde_json::Value) -> Result<Val> {
11    let v: Val = serde_json::from_value(value.clone())?;
12    Ok(v)
13}
14
15fn from_jaq_val(value: &Val) -> Result<serde_json::Value> {
16    let mut buf = Vec::new();
17    write::write(&mut buf, &write::Pp::default(), 0, value).context("write jaq value as JSON")?;
18    let v: serde_json::Value = serde_json::from_slice(&buf).context("parse jaq output as JSON")?;
19    Ok(v)
20}
21
22fn compile_filter<'s>(
23    expr: &'s str,
24    global_vars: impl IntoIterator<Item = &'s str>,
25) -> Result<jaq_core::compile::Filter<jaq_core::Native<data::JustLut<Val>>>> {
26    let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs()));
27    let arena = Arena::default();
28    let modules = loader
29        .load(
30            &arena,
31            File {
32                code: expr,
33                path: (),
34            },
35        )
36        .map_err(|errs| anyhow::anyhow!("{errs:?}"))
37        .with_context(|| format!("jq parse failed: {expr:?}"))?;
38
39    let compiler = jaq_core::Compiler::default()
40        .with_funs(jaq_std::funs().chain(jaq_json::funs()))
41        .with_global_vars(global_vars);
42
43    compiler
44        .compile(modules)
45        .map_err(|errs| anyhow::anyhow!("{errs:?}"))
46        .with_context(|| format!("jq compile failed: {expr:?}"))
47}
48
49pub fn query(value: &serde_json::Value, expr: &str) -> Result<Vec<serde_json::Value>> {
50    query_with_vars(value, expr, &BTreeMap::new())
51}
52
53pub fn query_with_vars(
54    value: &serde_json::Value,
55    expr: &str,
56    vars: &BTreeMap<String, serde_json::Value>,
57) -> Result<Vec<serde_json::Value>> {
58    let input = to_jaq_val(value).context("convert input JSON to jq value")?;
59
60    let global_var_names: Vec<String> = vars.keys().map(|k| format!("${k}")).collect();
61    let global_var_slices: Vec<&str> = global_var_names.iter().map(String::as_str).collect();
62    let filter = compile_filter(expr, global_var_slices)?;
63
64    let mut global_var_values: Vec<Val> = Vec::with_capacity(vars.len());
65    for v in vars.values() {
66        global_var_values.push(to_jaq_val(v)?);
67    }
68
69    let ctx = Ctx::<data::JustLut<Val>>::new(&filter.lut, Vars::new(global_var_values));
70
71    let mut out = Vec::new();
72    for y in filter
73        .id
74        .run((ctx, input))
75        .map(unwrap_valr)
76        .collect::<Vec<_>>()
77    {
78        let y = y
79            .map_err(|e| anyhow::anyhow!("{e:?}"))
80            .context("jq runtime error")?;
81        out.push(from_jaq_val(&y).context("convert jq output to JSON")?);
82    }
83
84    Ok(out)
85}
86
87/// Evaluate a jq expression like `jq -e`: returns `true` if the last output value is truthy.
88///
89/// Truthiness matches jq:
90/// - `false` and `null` are falsey
91/// - all other values are truthy
92/// - no output is treated as falsey
93pub fn eval_exit_status(value: &serde_json::Value, expr: &str) -> Result<bool> {
94    let out = query(value, expr)?;
95    let Some(last) = out.last() else {
96        return Ok(false);
97    };
98    Ok(!matches!(
99        last,
100        serde_json::Value::Null | serde_json::Value::Bool(false)
101    ))
102}
103
104/// Evaluate a jq expression and return raw output lines similar to `jq -r`.
105///
106/// - strings are emitted without JSON quotes
107/// - all other types are emitted as compact JSON per line
108pub fn query_raw(value: &serde_json::Value, expr: &str) -> Result<Vec<String>> {
109    let out = query(value, expr)?;
110    let mut lines = Vec::with_capacity(out.len());
111    for v in out {
112        match v {
113            serde_json::Value::String(s) => lines.push(s),
114            other => lines.push(serde_json::to_string(&other)?),
115        }
116    }
117    Ok(lines)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use pretty_assertions::assert_eq;
124
125    #[test]
126    fn jq_basic_query_outputs_multiple_values() {
127        let input = serde_json::json!(["a", "b"]);
128        let out = query(&input, ".[]").unwrap();
129        assert_eq!(out, vec![serde_json::json!("a"), serde_json::json!("b")]);
130    }
131
132    #[test]
133    fn jq_eval_exit_status_matches_truthiness() {
134        let input = serde_json::json!({"a": 1});
135        assert!(eval_exit_status(&input, ".a == 1").unwrap());
136        assert!(!eval_exit_status(&input, ".a == 2").unwrap());
137        assert!(!eval_exit_status(&input, "empty").unwrap());
138    }
139
140    #[test]
141    fn jq_vars_support_arg_like_usage() {
142        let input = serde_json::json!({"data": {"login": {"accessToken": "t"}}});
143        let mut vars = BTreeMap::new();
144        vars.insert("field".to_string(), serde_json::json!("login"));
145
146        let out = query_with_vars(&input, ".data[$field].accessToken", &vars).unwrap();
147        assert_eq!(out, vec![serde_json::json!("t")]);
148    }
149
150    #[test]
151    fn jq_query_raw_unwraps_strings() {
152        let input = serde_json::json!({"token": "abc"});
153        let out = query_raw(&input, ".token").unwrap();
154        assert_eq!(out, vec!["abc".to_string()]);
155    }
156
157    #[test]
158    fn jq_parse_errors_include_expression() {
159        let input = serde_json::json!({});
160        let err = query(&input, ".[").unwrap_err();
161        assert!(format!("{err:#}").contains(".["));
162    }
163}