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
93pub 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
110pub 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}