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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
42 changes: 33 additions & 9 deletions examples/simple-cli.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

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(())
}
84 changes: 76 additions & 8 deletions src/jq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -86,10 +88,20 @@ impl Jq {

/// Run the jq program against an input.
pub fn execute(&mut self, input: CString) -> Result<String> {
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<Chunk>(
&mut self,
inputs: impl Iterator<Item = Chunk>,
try_into_cstr: impl Fn(&Chunk) -> Result<&CStr>,
) -> Result<String> {
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
Expand Down Expand Up @@ -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 {
Expand All @@ -203,12 +220,12 @@ impl Parser {
}
}

pub fn parse(&mut self, input: CString) -> Result<JV> {
pub fn parse(self, input: CString) -> Result<JV> {
// 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
Expand All @@ -220,7 +237,7 @@ impl Parser {
self.ptr,
input.as_ptr(),
input.as_bytes().len() as i32,
is_last,
is_partial,
)
};

Expand All @@ -239,6 +256,57 @@ impl Parser {
})
}
}

pub fn parse_slurped<Chunk>(
mut self,
inputs: impl Iterator<Item = Chunk>,
try_into_cstr: impl Fn(&Chunk) -> Result<&CStr>,
) -> Result<JV> {
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 {
Expand Down
9 changes: 9 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = &'a str>) -> Result<String> {
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.
Expand Down
6 changes: 6 additions & 0 deletions tests/slurp.rs
Original file line number Diff line number Diff line change
@@ -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]");
}