diff --git a/Cargo.toml b/Cargo.toml index d50243f..2000508 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ criterion = "0.2" serde_json = "1.0" matches = "0.1.8" error-chain = "0.12.*" +clap = { version = "4.5.4", features = ["derive"] } [package.metadata.docs.rs] features = ["bundled"] diff --git a/examples/simple-cli.rs b/examples/simple-cli.rs index a1717e4..77d756f 100644 --- a/examples/simple-cli.rs +++ b/examples/simple-cli.rs @@ -1,13 +1,37 @@ -extern crate jq_rs; -use std::env; +use clap::Parser; -fn main() { - let mut args = env::args().skip(1); +#[derive(Parser)] +struct Args { + #[arg(short)] + slurp: bool, + /// Concatenate multiple inputs together directly, instead of treating them as separate inputs. + #[arg(long)] + concat: bool, + program: String, + inputs: Vec, +} + +fn main() -> jq_rs::Result<()> { + let args = Args::parse(); - let program = args.next().expect("jq program"); - let input = args.next().expect("data input"); - match jq_rs::run(&program, &input) { - Ok(s) => print!("{}", s), // The output will include a trailing newline - Err(e) => eprintln!("{}", e), + let mut program = jq_rs::compile(&args.program)?; + + if args.slurp { + match program.run_slurp( + args.inputs + .iter() + .map(String::as_str) + .flat_map(|input| [input, if args.concat { "" } else { "\n" }]), + ) { + Ok(s) => print!("{}", s), // The output will include a trailing newline + Err(e) => eprintln!("{}", e), + } + } else { + match program.run(args.inputs.get(0).map(String::as_str).unwrap_or("")) { + Ok(s) => print!("{}", s), // The output will include a trailing newline + Err(e) => eprintln!("{}", e), + } } + + Ok(()) } diff --git a/src/jq.rs b/src/jq.rs index 7e0e114..5d817da 100644 --- a/src/jq.rs +++ b/src/jq.rs @@ -6,13 +6,15 @@ use crate::errors::{Error, Result}; use jq_sys::{ jq_compile, jq_format_error, jq_get_exit_code, jq_halted, jq_init, jq_next, jq_set_error_cb, - 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, + jq_start, jq_state, jq_teardown, jv, jv_array, jv_array_append, 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_remaining, jv_parser_set_buf, jv_string_value, }; use std::ffi::{CStr, CString}; +use std::mem::ManuallyDrop; use std::os::raw::{c_char, c_void}; +use std::ptr; pub struct Jq { state: *mut jq_state, @@ -86,10 +88,20 @@ impl Jq { /// Run the jq program against an input. pub fn execute(&mut self, input: CString) -> Result { - let mut parser = Parser::new(); + let parser = Parser::new(); self.process(parser.parse(input)?) } + /// Run the jq program against an iterator of inputs that would get slurped. + pub fn execute_slurped( + &mut self, + inputs: impl Iterator, + try_into_cstr: impl Fn(&Chunk) -> Result<&CStr>, + ) -> Result { + let parser = Parser::new(); + self.process(parser.parse_slurped(inputs, try_into_cstr)?) + } + /// Unwind the parser and return the rendered result. /// /// When this results in `Err`, the String value should contain a message about @@ -184,6 +196,11 @@ impl JV { pub fn invalid_has_msg(&self) -> bool { unsafe { jv_invalid_has_msg(jv_copy(self.ptr)) == 1 } } + + pub fn into_raw(self) -> jv { + let this = ManuallyDrop::new(self); + this.ptr + } } impl Drop for JV { @@ -203,12 +220,12 @@ impl Parser { } } - pub fn parse(&mut self, input: CString) -> Result { + pub fn parse(self, input: CString) -> Result { // For a single run, we could set this to `1` (aka `true`) but this will // break the repeated `JqProgram` usage. // It may be worth exposing this to the caller so they can set it for each // use case, but for now we'll just "leave it open." - let is_last = 0; + let is_partial = 0; // Originally I planned to have a separate "set_buf" method, but it looks like // the C api really wants you to set the buffer, then call `jv_parser_next()` in @@ -220,7 +237,7 @@ impl Parser { self.ptr, input.as_ptr(), input.as_bytes().len() as i32, - is_last, + is_partial, ) }; @@ -239,6 +256,57 @@ impl Parser { }) } } + + pub fn parse_slurped( + mut self, + inputs: impl Iterator, + try_into_cstr: impl Fn(&Chunk) -> Result<&CStr>, + ) -> Result { + let mut slurped = JV { + ptr: unsafe { jv_array() }, + }; + + let mut peekable = inputs.peekable(); + while let Some(input) = peekable.next() { + // The early return will call the destructor of `slurped`, which drops the allocated array jv. + // The previous `input` stored under `self.ptr` is also freed whe `self` is dropped. + let input = try_into_cstr(&input)?; + + unsafe { + jv_parser_set_buf( + self.ptr, + input.as_ptr(), + input.to_bytes().len() as i32, + if peekable.peek().is_some() { 1 } else { 0 }, + ); + } + + while self.remaining() != 0 { + let value = JV { + ptr: unsafe { jv_parser_next(self.ptr) }, + }; + if value.is_valid() { + slurped.ptr = unsafe { jv_array_append(slurped.ptr, value.into_raw()) }; + } else if unsafe { jv_invalid_has_msg(jv_copy(value.ptr)) } == 1 { + // `self.ptr`, `slurped` and `value` will be independently dropped after this + // return. + return Err(Error::System { + reason: Some( + value + .get_msg() + .unwrap_or_else(|| "JQ: Parser error".to_string()), + ), + }); + } + } + } + + Ok(slurped) + } + + fn remaining(&mut self) -> i32 { + unsafe { jv_parser_remaining(self.ptr) } + } } impl Drop for Parser { diff --git a/src/lib.rs b/src/lib.rs index 1f4c90f..15c2abb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,6 +182,15 @@ impl JqProgram { let input = CString::new(data)?; self.jq.execute(input) } + + /// Runs an iterator of json string inputs as a slurped array against a pre-compiled jq program. + pub fn run_slurp<'a>(&mut self, inputs: impl IntoIterator) -> Result { + self.jq + .execute_slurped(inputs.into_iter().map(CString::new), |result| match result { + Ok(string) => Ok(&**string), + Err(err) => Err(Error::from(err.clone())), + }) + } } /// Compile a jq program then reuse it, running several inputs against it. diff --git a/tests/slurp.rs b/tests/slurp.rs new file mode 100644 index 0000000..c224eb9 --- /dev/null +++ b/tests/slurp.rs @@ -0,0 +1,6 @@ +#[test] +fn test_slurp() { + let mut program = jq_rs::compile(".").unwrap(); + let result = program.run_slurp(["123", "4567", " ", "890"]).unwrap(); + assert_eq!(result.as_str().trim(), "[1234567,890]"); +}