use std::path::Path;
use async_trait::async_trait;
use jaq_core::{load, compile, Ctx, RcIter};
use jaq_json::Val;
use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData};
use crate::tools::{ExecContext, ParamSchema, Tool, ToolArgs, ToolSchema};
pub struct JqNative;
type Filter = jaq_core::Filter<jaq_core::Native<Val>>;
fn compile_filter(filter_str: &str, global_vars: &[String]) -> Result<Filter, String> {
let arena = load::Arena::default();
let defs = jaq_std::defs().chain(jaq_json::defs());
let loader = load::Loader::new(defs);
let modules = loader
.load(&arena, load::File { path: (), code: filter_str })
.map_err(|errs| {
let msgs: Vec<String> = errs
.into_iter()
.flat_map(|(_, e)| -> Vec<String> {
match e {
load::Error::Io(io_errs) => io_errs.into_iter().map(|(_, msg)| msg).collect(),
load::Error::Lex(lex_errs) => lex_errs.into_iter().map(|(expected, _)| format!("expected {}", expected.as_str())).collect(),
load::Error::Parse(parse_errs) => parse_errs.into_iter().map(|(expected, _)| format!("expected {}", expected.as_str())).collect(),
}
})
.collect();
format!("jq parse error: {}", msgs.join(", "))
})?;
let funs = jaq_std::funs().chain(jaq_json::funs());
let compiler = compile::Compiler::default()
.with_funs(funs)
.with_global_vars(global_vars.iter().map(String::as_str));
let filter = compiler.compile(modules).map_err(|errs| {
let msgs: Vec<String> = errs
.into_iter()
.flat_map(|(_, errors)| {
errors.into_iter().map(|(_, undefined)| format!("undefined {}", undefined.as_str()))
})
.collect();
format!("jq compile error: {}", msgs.join(", "))
})?;
Ok(filter)
}
struct JqRun {
text: String,
values: Vec<serde_json::Value>,
}
fn execute_filter_json(
filter: &Filter,
json: serde_json::Value,
raw_output: bool,
var_values: Vec<serde_json::Value>,
) -> Result<JqRun, String> {
let input_val = json_to_val(json);
let vars: Vec<Val> = var_values.into_iter().map(json_to_val).collect();
let inputs: RcIter<_> = RcIter::new(Box::new(core::iter::empty()));
let ctx = Ctx::new(vars, &inputs);
let results: Vec<Result<Val, jaq_core::Error<Val>>> = filter
.run((ctx, input_val))
.collect();
let mut text = String::new();
let mut values = Vec::with_capacity(results.len());
for result in results {
match result {
Ok(val) => {
let formatted = if raw_output {
format_raw(&val)
} else {
format_json(&val)
};
if !text.is_empty() {
text.push('\n');
}
text.push_str(&formatted);
values.push(val_to_json(&val));
}
Err(e) => {
return Err(format!("jq runtime error: {}", e));
}
}
}
Ok(JqRun { text, values })
}
fn json_to_val(json: serde_json::Value) -> Val {
use std::rc::Rc;
match json {
serde_json::Value::Null => Val::Null,
serde_json::Value::Bool(b) => Val::Bool(b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
if let Ok(i) = isize::try_from(i) {
Val::Int(i)
} else {
Val::Num(Rc::new(n.to_string()))
}
} else if let Some(f) = n.as_f64() {
Val::Float(f)
} else {
Val::Num(Rc::new(n.to_string()))
}
}
serde_json::Value::String(s) => Val::Str(Rc::new(s)),
serde_json::Value::Array(arr) => Val::Arr(Rc::new(arr.into_iter().map(json_to_val).collect())),
serde_json::Value::Object(obj) => {
Val::obj(obj.into_iter().map(|(k, v)| (Rc::new(k), json_to_val(v))).collect())
}
}
}
fn format_raw(val: &Val) -> String {
match val {
Val::Str(s) => s.to_string(),
Val::Null => "null".to_string(),
Val::Bool(b) => b.to_string(),
Val::Int(n) => n.to_string(),
Val::Float(n) => format!("{:?}", n),
Val::Num(s) => s.to_string(),
Val::Arr(arr) => {
let items: Vec<String> = arr.iter().map(format_raw).collect();
items.join("\n")
}
Val::Obj(_) => serde_json::to_string(&val_to_json(val)).unwrap_or_default(),
}
}
fn format_json(val: &Val) -> String {
serde_json::to_string_pretty(&val_to_json(val)).unwrap_or_default()
}
fn ast_value_to_json(value: &Value) -> serde_json::Value {
match value {
Value::Null => serde_json::Value::Null,
Value::Bool(b) => serde_json::Value::Bool(*b),
Value::Int(i) => serde_json::Value::Number((*i).into()),
Value::Float(f) => {
serde_json::Number::from_f64(*f)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null)
}
Value::String(s) => {
serde_json::from_str(s).unwrap_or_else(|_| serde_json::Value::String(s.clone()))
}
Value::Json(json) => json.clone(),
Value::Blob(blob) => {
let mut map = serde_json::Map::new();
map.insert("_type".to_string(), serde_json::Value::String("blob".to_string()));
map.insert("id".to_string(), serde_json::Value::String(blob.id.clone()));
map.insert("size".to_string(), serde_json::Value::Number(blob.size.into()));
map.insert("contentType".to_string(), serde_json::Value::String(blob.content_type.clone()));
serde_json::Value::Object(map)
}
}
}
fn val_to_json(val: &Val) -> serde_json::Value {
match val {
Val::Null => serde_json::Value::Null,
Val::Bool(b) => serde_json::Value::Bool(*b),
Val::Int(n) => serde_json::Value::Number((*n as i64).into()),
Val::Float(n) => serde_json::Number::from_f64(*n)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
Val::Num(s) => {
serde_json::from_str(s).unwrap_or(serde_json::Value::String(s.to_string()))
}
Val::Str(s) => serde_json::Value::String(s.to_string()),
Val::Arr(arr) => serde_json::Value::Array(arr.iter().map(val_to_json).collect()),
Val::Obj(obj) => {
let map: serde_json::Map<String, serde_json::Value> = obj
.iter()
.map(|(k, v)| (k.to_string(), val_to_json(v)))
.collect();
serde_json::Value::Object(map)
}
}
}
#[async_trait]
impl Tool for JqNative {
fn name(&self) -> &str {
"jq"
}
fn schema(&self) -> ToolSchema {
ToolSchema::new(
"jq",
"JSON query processor — built into kaish (native jaq, no external binary). \
The canonical way to extract fields from JSON: pipe data in, read \
from a variable via `jq '.field' <<< \"$VAR\"`, or bind kaish \
variables into the filter with `--arg` / `--argjson` plus `-n`.",
)
.param(ParamSchema::required(
"filter",
"string",
"jq filter expression",
))
.param(ParamSchema::optional(
"raw",
"bool",
Value::Bool(false),
"Raw output mode (-r): output strings without quotes",
))
.param(ParamSchema::optional(
"compact",
"bool",
Value::Bool(false),
"Compact output mode (-c): no pretty-printing",
))
.param(ParamSchema::optional(
"path",
"string",
Value::String("".into()),
"Read from VFS file instead of stdin",
))
.param(
ParamSchema::optional(
"arg",
"array",
Value::Null,
"Bind a kaish variable as a jq string: --arg NAME VALUE → $NAME. Repeatable.",
)
.consumes(2),
)
.param(
ParamSchema::optional(
"argjson",
"array",
Value::Null,
"Bind a kaish variable as a jq JSON value: --argjson NAME JSON → $NAME. Repeatable.",
)
.consumes(2),
)
.param(
ParamSchema::optional(
"null-input",
"bool",
Value::Bool(false),
"Use null as input instead of reading stdin (-n / --null-input).",
)
.with_aliases(["-n"]),
)
.example("Extract a field", "cat data.json | jq '.name'")
.example("Raw string output", "cat data.json | jq -r '.version'")
.example("Filter an array", "cat items.json | jq '.[] | select(.active)'")
.example("Read JSON from a variable", r#"jq -r '.name' <<< "$RESULT""#)
.example(
"Bind a kaish variable into the filter",
r#"R='{"x":42}'; jq -n --argjson r "$R" '$r.x'"#,
)
}
async fn execute(&self, args: ToolArgs, ctx: &mut ExecContext) -> ExecResult {
let filter_str = match args.get_string("filter", 0) {
Some(f) => f,
None => return ExecResult::failure(1, "jq: filter expression required"),
};
let (global_var_names, global_var_values) = match collect_bindings(&args) {
Ok(pair) => pair,
Err(e) => return ExecResult::failure(1, e),
};
let filter = match compile_filter(&filter_str, &global_var_names) {
Ok(f) => f,
Err(e) => return ExecResult::failure(1, e),
};
let raw_output = args.has_flag("raw") || args.has_flag("r");
let _compact = args.has_flag("compact") || args.has_flag("c");
let null_input = args.has_flag("null-input") || args.has_flag("n");
let input_json: serde_json::Value = if null_input {
serde_json::Value::Null
} else if let Some(path) = args.get_string("path", 1) {
if !path.is_empty() {
let resolved = ctx.resolve_path(&path);
match ctx.backend.read(Path::new(&resolved), None).await {
Ok(bytes) => {
let text = String::from_utf8_lossy(&bytes);
match serde_json::from_str(&text) {
Ok(json) => json,
Err(e) => return ExecResult::failure(1, format!("jq: invalid JSON in {}: {}", path, e)),
}
}
Err(e) => {
return ExecResult::failure(1, format!("jq: failed to read {}: {}", path, e))
}
}
} else if let Some(data) = ctx.take_stdin_data() {
ast_value_to_json(&data)
} else if let Some(text) = ctx.read_stdin_to_string().await {
match serde_json::from_str(&text) {
Ok(json) => json,
Err(e) => return ExecResult::failure(1, format!("jq: invalid JSON input: {}", e)),
}
} else {
return ExecResult::failure(1, "jq: no input provided");
}
} else if let Some(data) = ctx.take_stdin_data() {
ast_value_to_json(&data)
} else if let Some(text) = ctx.read_stdin_to_string().await {
match serde_json::from_str(&text) {
Ok(json) => json,
Err(e) => return ExecResult::failure(1, format!("jq: invalid JSON input: {}", e)),
}
} else {
return ExecResult::failure(1, "jq: no input provided");
};
match execute_filter_json(&filter, input_json, raw_output, global_var_values) {
Ok(run) => build_exec_result(run),
Err(e) => ExecResult::failure(1, e),
}
}
}
fn collect_bindings(args: &ToolArgs) -> Result<(Vec<String>, Vec<serde_json::Value>), String> {
let mut names: Vec<String> = Vec::new();
let mut values: Vec<serde_json::Value> = Vec::new();
if let Some(Value::Json(serde_json::Value::Array(occurrences))) = args.named.get("arg") {
for occ in occurrences {
let (name, raw) = extract_pair(occ, "arg")?;
names.push(format!("${name}"));
values.push(serde_json::Value::String(raw));
}
}
if let Some(Value::Json(serde_json::Value::Array(occurrences))) = args.named.get("argjson") {
for occ in occurrences {
let (name, raw) = extract_pair(occ, "argjson")?;
let parsed: serde_json::Value = serde_json::from_str(&raw)
.map_err(|e| format!("jq: --argjson {name}: invalid JSON: {e}"))?;
names.push(format!("${name}"));
values.push(parsed);
}
}
Ok((names, values))
}
fn extract_pair(occ: &serde_json::Value, flag: &str) -> Result<(String, String), String> {
match occ {
serde_json::Value::Array(pair) if pair.len() == 2 => {
let name = value_as_string(&pair[0])
.ok_or_else(|| format!("jq: --{flag} NAME must be a string"))?;
let value = value_as_string(&pair[1])
.ok_or_else(|| format!("jq: --{flag} VALUE must be a string"))?;
Ok((name, value))
}
other => Err(format!(
"jq: internal error: --{flag} expected 2-element pair, got {other}"
)),
}
}
fn value_as_string(v: &serde_json::Value) -> Option<String> {
match v {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Number(n) => Some(n.to_string()),
serde_json::Value::Bool(b) => Some(b.to_string()),
_ => Some(v.to_string()),
}
}
fn build_exec_result(run: JqRun) -> ExecResult {
use crate::interpreter::json_to_value;
let JqRun { text, mut values } = run;
if values.is_empty() {
return ExecResult::with_output(OutputData::text(text));
}
if values.len() == 1 {
if let Some(only) = values.pop() {
return ExecResult::success_with_data(text, json_to_value(only));
}
}
ExecResult::success_with_data(text, Value::Json(serde_json::Value::Array(values)))
}
#[cfg(test)]
fn validate_filter(filter: &str) -> Result<(), String> {
compile_filter(filter, &[])?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::WriteMode;
use crate::vfs::{MemoryFs, VfsRouter};
use std::sync::Arc;
fn make_ctx() -> ExecContext {
let mut vfs = VfsRouter::new();
vfs.mount("/", MemoryFs::new());
ExecContext::new(Arc::new(vfs))
}
#[test]
fn test_validate_filter_valid() {
assert!(validate_filter(".name").is_ok());
assert!(validate_filter(".items[]").is_ok());
assert!(validate_filter(".[] | select(.active)").is_ok());
assert!(validate_filter("map(.x + 1)").is_ok());
}
#[test]
fn test_validate_filter_invalid() {
assert!(validate_filter(".[[[invalid").is_err());
assert!(validate_filter(".foo | | bar").is_err());
}
#[tokio::test]
async fn test_jq_native_simple_filter() {
let mut ctx = make_ctx();
ctx.set_stdin(r#"{"name": "Alice"}"#.to_string());
let mut args = ToolArgs::new();
args.positional.push(Value::String(".name".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(result.ok(), "jq failed: {}", result.err);
assert_eq!(result.text_out().trim(), "\"Alice\"");
}
#[tokio::test]
async fn test_jq_native_raw_output() {
let mut ctx = make_ctx();
ctx.set_stdin(r#"{"name": "Alice"}"#.to_string());
let mut args = ToolArgs::new();
args.positional.push(Value::String(".name".into()));
args.flags.insert("r".to_string());
let result = JqNative.execute(args, &mut ctx).await;
assert!(result.ok(), "jq failed: {}", result.err);
assert_eq!(result.text_out().trim(), "Alice");
}
#[tokio::test]
async fn test_jq_native_array_iteration() {
let mut ctx = make_ctx();
ctx.set_stdin(r#"[1, 2, 3]"#.to_string());
let mut args = ToolArgs::new();
args.positional.push(Value::String(".[]".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(result.ok(), "jq failed: {}", result.err);
assert_eq!(result.text_out().trim(), "1\n2\n3");
}
#[tokio::test]
async fn test_jq_native_invalid_json() {
let mut ctx = make_ctx();
ctx.set_stdin("not valid json".to_string());
let mut args = ToolArgs::new();
args.positional.push(Value::String(".".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(!result.ok());
assert!(result.err.contains("invalid JSON"));
}
#[tokio::test]
async fn test_jq_native_invalid_filter() {
let mut ctx = make_ctx();
ctx.set_stdin(r#"{"a": 1}"#.to_string());
let mut args = ToolArgs::new();
args.positional.push(Value::String(".[[[invalid".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(!result.ok());
}
#[tokio::test]
async fn test_jq_native_no_input() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::String(".".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(!result.ok());
assert!(result.err.contains("no input"));
}
#[tokio::test]
async fn test_jq_native_from_vfs_file() {
let mut ctx = make_ctx();
ctx.backend
.write(Path::new("/test.json"), br#"{"value": 42}"#, WriteMode::Overwrite)
.await
.expect("failed to write test file");
let mut args = ToolArgs::new();
args.positional.push(Value::String(".value".into()));
args.named
.insert("path".to_string(), Value::String("/test.json".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(result.ok(), "jq failed: {}", result.err);
assert_eq!(result.text_out().trim(), "42");
}
#[tokio::test]
async fn test_jq_positional_file_argument() {
let mut ctx = make_ctx();
ctx.backend
.write(
Path::new("/tmp/data.json"),
br#"{"result": [1, 2, 3]}"#,
WriteMode::Overwrite,
)
.await
.unwrap();
let mut args = ToolArgs::new();
args.positional.push(Value::String(".result[]".into()));
args.positional.push(Value::String("/tmp/data.json".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(result.ok(), "jq positional file failed: {}", result.err);
assert_eq!(result.text_out().trim(), "1\n2\n3");
}
#[tokio::test]
async fn test_jq_select_with_positional_file() {
let mut ctx = make_ctx();
ctx.backend
.write(
Path::new("/query.json"),
br#"[{"id": 1, "active": true}, {"id": 2, "active": false}, {"id": 3, "active": true}]"#,
WriteMode::Overwrite,
)
.await
.unwrap();
let mut args = ToolArgs::new();
args.positional
.push(Value::String("[.[] | select(.active)] | length".into()));
args.positional.push(Value::String("/query.json".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(result.ok(), "jq select failed: {}", result.err);
assert_eq!(result.text_out().trim(), "2");
}
#[tokio::test]
async fn test_jq_type_check_positional_file() {
let mut ctx = make_ctx();
ctx.backend
.write(Path::new("/check.json"), br#"[1, 2, 3]"#, WriteMode::Overwrite)
.await
.unwrap();
let mut args = ToolArgs::new();
args.positional.push(Value::String("type".into()));
args.positional.push(Value::String("/check.json".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(result.ok(), "jq type check failed: {}", result.err);
assert_eq!(result.text_out().trim(), "\"array\"");
}
#[tokio::test]
async fn test_jq_index_access_positional_file() {
let mut ctx = make_ctx();
ctx.backend
.write(
Path::new("/arr.json"),
br#"[{"name": "first"}, {"name": "second"}]"#,
WriteMode::Overwrite,
)
.await
.unwrap();
let mut args = ToolArgs::new();
args.positional.push(Value::String(".[0]".into()));
args.positional.push(Value::String("/arr.json".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(result.ok(), "jq index failed: {}", result.err);
assert!(result.text_out().contains("\"name\": \"first\""));
}
#[tokio::test]
async fn test_jq_raw_output_with_positional_file() {
let mut ctx = make_ctx();
ctx.backend
.write(
Path::new("/person.json"),
br#"{"name": "Alice"}"#,
WriteMode::Overwrite,
)
.await
.unwrap();
let mut args = ToolArgs::new();
args.positional.push(Value::String(".name".into()));
args.positional.push(Value::String("/person.json".into()));
args.flags.insert("r".to_string());
let result = JqNative.execute(args, &mut ctx).await;
assert!(result.ok(), "jq -r failed: {}", result.err);
assert_eq!(result.text_out().trim(), "Alice"); }
#[tokio::test]
async fn test_jq_stdin_still_works() {
let mut ctx = make_ctx();
ctx.set_stdin(r#"{"value": 42}"#.to_string());
let mut args = ToolArgs::new();
args.positional.push(Value::String(".value".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(result.ok(), "jq stdin failed: {}", result.err);
assert_eq!(result.text_out().trim(), "42");
}
#[tokio::test]
async fn test_jq_named_path_still_works() {
let mut ctx = make_ctx();
ctx.backend
.write(Path::new("/named.json"), br#"{"x": 99}"#, WriteMode::Overwrite)
.await
.unwrap();
let mut args = ToolArgs::new();
args.positional.push(Value::String(".x".into()));
args.named
.insert("path".to_string(), Value::String("/named.json".into()));
let result = JqNative.execute(args, &mut ctx).await;
assert!(result.ok(), "jq path= failed: {}", result.err);
assert_eq!(result.text_out().trim(), "99");
}
}