Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.*"

Expand Down
159 changes: 158 additions & 1 deletion src/jq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -111,6 +117,54 @@ impl Jq {

Ok(buf)
}

pub fn execute_json(&mut self, input: &serde_json::Value) -> Result<serde_json::Value> {
let input_jv = JV::from_serde(input)?;
self.process_jv(input_jv)
}

fn process_jv(&mut self, initial_value: JV) -> Result<serde_json::Value> {
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 {
Expand Down Expand Up @@ -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<JV> {
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<Value> {
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 {
Expand Down
67 changes: 67 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<serde_json::Value> {
self.jq.execute_json(data)
}
}

/// Compile a jq program then reuse it, running several inputs against it.
Expand Down Expand Up @@ -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);
}

}