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