diff --git a/Cargo.toml b/Cargo.toml index d50243f..514dc71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,10 @@ bundled = ["jq-sys/bundled"] [dependencies] jq-sys = "0.2.*" +serde_json = "1.0" [dev-dependencies] criterion = "0.2" -serde_json = "1.0" matches = "0.1.8" error-chain = "0.12.*" diff --git a/src/jq.rs b/src/jq.rs index 7e0e114..6d39eb7 100644 --- a/src/jq.rs +++ b/src/jq.rs @@ -9,10 +9,16 @@ use jq_sys::{ jq_start, jq_state, jq_teardown, jv, jv_copy, jv_dump_string, jv_free, jv_get_kind, jv_invalid_get_msg, jv_invalid_has_msg, jv_kind_JV_KIND_INVALID, jv_kind_JV_KIND_NUMBER, jv_kind_JV_KIND_STRING, jv_number_value, jv_parser, jv_parser_free, jv_parser_new, - jv_parser_next, jv_parser_set_buf, jv_string_value, + jv_parser_next, jv_parser_set_buf, jv_string_value, jv_null, jv_true, jv_false, jv_number, jv_string, + jv_array, jv_array_append, jv_array_length, jv_array_get, + jv_object, jv_object_set, jv_object_iter, jv_object_iter_next, + jv_object_iter_key, jv_object_iter_value, jv_object_iter_valid, + jv_kind_JV_KIND_NULL, jv_kind_JV_KIND_FALSE, jv_kind_JV_KIND_TRUE, + jv_kind_JV_KIND_ARRAY, jv_kind_JV_KIND_OBJECT }; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_void}; +use serde_json::Value; pub struct Jq { state: *mut jq_state, @@ -111,6 +117,54 @@ impl Jq { Ok(buf) } + + pub fn execute_json(&mut self, input: &serde_json::Value) -> Result { + let input_jv = JV::from_serde(input)?; + self.process_jv(input_jv) + } + + fn process_jv(&mut self, initial_value: JV) -> Result { + unsafe { + // jq_start consumes the jv passed to it. + // We pass a copy so that the 'initial_value' wrapper can safely drop its own reference later. + jq_start(self.state, jv_copy(initial_value.ptr), 0); + } + + let mut result = serde_json::Value::Null; + + // 1. Get the first item from the stream + let mut next = JV { + ptr: unsafe { jq_next(self.state) }, + }; + + // 2. Loop only while the item is VALID + while next.is_valid() { + // Since it is valid, it is safe to convert to serde (and NOT safe to check get_msg) + result = next.to_serde()?; + + // Get the next item + next = JV { + ptr: unsafe { jq_next(self.state) }, + }; + } + + // 3. The loop has exited, so 'next' is now INVALID. + // It is now safe and correct to check it for error messages. + + if self.is_halted() { + use ExitCode::*; + match self.get_exit_code() { + JQ_ERROR_SYSTEM => return Err(Error::System { reason: next.get_msg() }), + JQ_ERROR_COMPILE => return Err(Error::InvalidProgram { reason: self.err_buf.clone() }), + JQ_ERROR_UNKNOWN => return Err(Error::Unknown), + _ => {} // OK + } + } else if let Some(msg) = next.get_msg() { + return Err(Error::System { reason: Some(msg) }); + } + + Ok(result) + } } impl Drop for Jq { @@ -184,6 +238,109 @@ impl JV { pub fn invalid_has_msg(&self) -> bool { unsafe { jv_invalid_has_msg(jv_copy(self.ptr)) == 1 } } + + /// Convert a serde_json::Value into a libjq jv value. + pub fn from_serde(value: &Value) -> Result { + let ptr = unsafe { + match value { + Value::Null => jv_null(), + Value::Bool(true) => jv_true(), + Value::Bool(false) => jv_false(), + Value::Number(n) => { + // jq numbers are doubles (f64) + if let Some(f) = n.as_f64() { + jv_number(f) + } else { + // Fallback for numbers that don't fit in f64 + jv_number(0.0) + } + }, + Value::String(s) => { + let c_str = CString::new(s.as_str()).map_err(|e| Error::StringConvert { err: Box::new(e) })?; + jv_string(c_str.as_ptr()) + }, + Value::Array(arr) => { + let mut jv_arr = jv_array(); + for item in arr { + let item_jv = JV::from_serde(item)?; + jv_arr = jv_array_append(jv_arr, jv_copy(item_jv.ptr)); + } + jv_arr + }, + Value::Object(obj) => { + let mut jv_obj = jv_object(); + for (k, v) in obj { + let k_c = CString::new(k.as_str()).map_err(|e| Error::StringConvert { err: Box::new(e) })?; + let v_jv = JV::from_serde(v)?; + jv_obj = jv_object_set(jv_obj, jv_string(k_c.as_ptr()), jv_copy(v_jv.ptr)); + } + jv_obj + } + } + }; + Ok(JV { ptr }) + } + + /// Convert a libjq jv value back into a serde_json::Value. + pub fn to_serde(&self) -> Result { + unsafe { + let kind = jv_get_kind(self.ptr); + match kind { + x if x == jv_kind_JV_KIND_NULL => Ok(Value::Null), + x if x == jv_kind_JV_KIND_FALSE => Ok(Value::Bool(false)), + x if x == jv_kind_JV_KIND_TRUE => Ok(Value::Bool(true)), + x if x == jv_kind_JV_KIND_NUMBER => { + let n = jv_number_value(self.ptr); + + // FIX: Check if the float is actually an integer + if n.fract() == 0.0 { + // It is a whole number. Try to fit it into an i64. + // We check bounds because f64 can represent values larger than i64::MAX + if n >= (i64::MIN as f64) && n <= (i64::MAX as f64) { + return Ok(Value::Number(serde_json::Number::from(n as i64))); + } + } + + // Fallback: It has a decimal part OR it's too big for i64 + Ok(serde_json::Number::from_f64(n).map(Value::Number).unwrap_or(Value::Null)) + }, + x if x == jv_kind_JV_KIND_STRING => { + let s = get_string_value(jv_string_value(self.ptr))?; + Ok(Value::String(s)) + }, + x if x == jv_kind_JV_KIND_ARRAY => { + let len = jv_array_length(jv_copy(self.ptr)); + let mut vec = Vec::with_capacity(len as usize); + for i in 0..len { + let item_ptr = jv_array_get(jv_copy(self.ptr), i); + let item = JV { ptr: item_ptr }; + vec.push(item.to_serde()?); + } + Ok(Value::Array(vec)) + }, + x if x == jv_kind_JV_KIND_OBJECT => { + let mut map = serde_json::Map::new(); + let mut iter = jv_object_iter(self.ptr); + while jv_object_iter_valid(self.ptr, iter) != 0 { + let key_ptr = jv_object_iter_key(self.ptr, iter); + let val_ptr = jv_object_iter_value(self.ptr, iter); + + let key = JV { ptr: key_ptr }; + let val = JV { ptr: val_ptr }; + + let key_str = key.as_string()?; + let val_json = val.to_serde()?; + + map.insert(key_str, val_json); + iter = jv_object_iter_next(self.ptr, iter); + } + Ok(Value::Object(map)) + }, + _ => Err(Error::Unknown), + } + } + } + } impl Drop for JV { diff --git a/src/lib.rs b/src/lib.rs index 1f4c90f..d170982 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,6 +182,11 @@ impl JqProgram { let input = CString::new(data)?; self.jq.execute(input) } + + /// Runs a serde_json::Value input against a pre-compiled jq program. + pub fn run_json(&mut self, data: &serde_json::Value) -> Result { + self.jq.execute_json(data) + } } /// Compile a jq program then reuse it, running several inputs against it. @@ -332,4 +337,66 @@ mod test { assert_matches!(res, Err(Error::System { .. })); } } + + #[test] + fn run_json_basic_types() { + // Test String + let input = json!({"val": "hello"}); + let mut prog = compile(".val").unwrap(); + assert_eq!(prog.run_json(&input).unwrap(), json!("hello")); + + // Test Number (Integer preserved as Number) + let input = json!({"val": 42}); + let mut prog = compile(".val").unwrap(); + assert_eq!(prog.run_json(&input).unwrap(), json!(42)); + + // Test Number (Float) + let input = json!({"val": 3.14}); + let mut prog = compile(".val").unwrap(); + assert_eq!(prog.run_json(&input).unwrap(), json!(3.14)); + + // Test Boolean + let input = json!({"val": true}); + let mut prog = compile(".val").unwrap(); + assert_eq!(prog.run_json(&input).unwrap(), json!(true)); + + // Test Null + let input = json!({"val": null}); + let mut prog = compile(".val").unwrap(); + assert_eq!(prog.run_json(&input).unwrap(), json!(null)); + } + + #[test] + fn run_json_complex_structures() { + let data = json!({ + "users": [ + {"id": 1, "name": "Alice", "active": true}, + {"id": 2, "name": "Bob", "active": false}, + {"id": 3, "name": "Charlie", "active": true} + ] + }); + + // 1. Filter and Map: Get names of active users + let mut prog = compile(".users | map(select(.active) | .name)").unwrap(); + let res = prog.run_json(&data).unwrap(); + assert_eq!(res, json!(["Alice", "Charlie"])); + + // 2. Object Construction: Create a summary object + let mut prog = compile("{ count: .users | length, first: .users[0].name }").unwrap(); + let res = prog.run_json(&data).unwrap(); + assert_eq!(res, json!({ "count": 3, "first": "Alice" })); + } + + #[test] + fn run_json_deep_nesting() { + // Verify that we don't lose data in deep recursion + let deep_json = json!({"level1": {"level2": {"level3": [1, 2, 3]}}}); + + // Identity transform should return the structure exactly as is + let mut prog = compile(".").unwrap(); + let res = prog.run_json(&deep_json).unwrap(); + + assert_eq!(res, deep_json); + } + }