json_query/
lib.rs

1#![deprecated(
2    since = "0.3.1",
3    note = "This is the final release of `json-query`. Future releases will be published as `jq-rs`."
4)]
5//! ## Overview
6//!
7//! [jq] is a command line tool which allows users to write small filter/transform
8//! programs with a special DSL to extract data from json.
9//!
10//! This crate provides bindings to the C API internals to give us programmatic
11//! access to this tool.
12//!
13//! For example, given a blob of json data, we can extract the values from
14//! the `id` field of a series of objects.
15//!
16//! ```
17//! let data = r#"{
18//!     "colors": [
19//!         {"id": 12, "name": "cyan"},
20//!         {"id": 34, "name": "magenta"},
21//!         {"id": 56, "name": "yellow"},
22//!         {"id": 78, "name": "black"}
23//!     ]
24//! }"#;
25//!
26//! let output = json_query::run("[.colors[].id]", data).unwrap();
27//! assert_eq!("[12,34,56,78]", &output);
28//! ```
29//!
30//! For times where you want to run the same jq program against multiple inputs, `compile()`
31//! returns a handle to the compiled jq program.
32//!
33//! ```
34//! let tv_shows = r#"[
35//!     {"title": "Twilight Zone"},
36//!     {"title": "X-Files"},
37//!     {"title": "The Outer Limits"}
38//! ]"#;
39//!
40//! let movies = r#"[
41//!     {"title": "The Omen"},
42//!     {"title": "Amityville Horror"},
43//!     {"title": "The Thing"}
44//! ]"#;
45//!
46//! let mut program = json_query::compile("[.[].title] | sort").unwrap();
47//!
48//! assert_eq!(
49//!     r#"["The Outer Limits","Twilight Zone","X-Files"]"#,
50//!     &program.run(tv_shows).unwrap()
51//! );
52//!
53//! assert_eq!(
54//!     r#"["Amityville Horror","The Omen","The Thing"]"#,
55//!     &program.run(movies).unwrap()
56//! );
57//! ```
58//!
59//! The output from these jq programs are returned as a string (just as is
60//! the case if you were using [jq] from the command-line), so be prepared to
61//! parse the output as needed after this step.
62//!
63//! Pairing this crate with something like [serde_json] might make a lot of
64//! sense.
65//!
66//! See the [jq site][jq] for details on the jq program syntax.
67//!
68//! ## Linking to `libjq`
69//!
70//! When the `bundled` feature is enabled (on by default) `libjq` is provided and
71//! linked statically by [jq-sys] and [jq-src]
72//! which require having autotools and gcc in `PATH` to build.
73//!
74//! If you disable the `bundled` feature, you will need to ensure your crate
75//! links to `libjq` in order for the bindings to work.
76//! For this you may need to add a `build.rs` script if you don't have one already.
77//!
78//! [jq]: https://stedolan.github.io/jq/
79//! [serde_json]: https://github.com/serde-rs/json
80//! [jq-sys]: https://github.com/onelson/jq-sys
81//! [jq-src]: https://github.com/onelson/jq-src
82//!
83extern crate jq_sys;
84#[cfg(test)]
85#[macro_use]
86extern crate serde_json;
87
88mod jq;
89
90use std::ffi::CString;
91use std::fmt;
92
93pub enum Error {
94    /// The jq program failed to compile.
95    Compile,
96    /// Indicates problems initializing the JQ state machine, or invalid values
97    /// produced by it.
98    System {
99        msg: Option<String>,
100    },
101    Unknown,
102}
103
104// FIXME: Until the next minor release, we won't be returning the Error variants
105//  back to the caller. Instead we'll be converting to `String` to maintain
106//  compatibility with the current signatures. When 0.4 is ready, revise these
107//  so they are more consistent (or adopt `failure`?)
108impl fmt::Display for Error {
109    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
110        let detail: String = match self {
111            Error::Compile => "syntax error: JQ Program failed to compile.".into(),
112            Error::System { msg } => msg
113                .as_ref()
114                .cloned()
115                .unwrap_or_else(|| "Unknown JQ Error".into()),
116            Error::Unknown => "Unknown JQ Error.".into(),
117        };
118        write!(f, "{}", detail)
119    }
120}
121
122/// Run a jq program on a blob of json data.
123///
124/// In the case of failure to run the program, feedback from the jq api will be
125/// available in the supplied `String` value.
126/// Failures can occur for a variety of reasons, but mostly you'll see them as
127/// a result of bad jq program syntax, or invalid json data.
128pub fn run(program: &str, data: &str) -> Result<String, String> {
129    compile(program)?.run(data)
130}
131
132/// A pre-compiled jq program which can be run against different inputs.
133pub struct JqProgram {
134    // lol
135    jq: jq::Jq,
136}
137
138impl JqProgram {
139    /// Runs a json string input against a pre-compiled jq program.
140    pub fn run(&mut self, data: &str) -> Result<String, String> {
141        if data.trim().is_empty() {
142            // During work on #4, #7, the parser test which allows us to avoid a memory
143            // error shows that an empty input just yields an empty response BUT our
144            // implementation would yield a parse error.
145            return Ok("".into());
146        }
147        let input =
148            CString::new(data).map_err(|_| "unable to convert data to c string.".to_string())?;
149        self.jq.execute(input).map_err(|e| {
150            // FIXME: string errors until v0.4
151            format!("{}", e)
152        })
153    }
154}
155
156/// Compile a jq program then reuse it, running several inputs against it.
157pub fn compile(program: &str) -> Result<JqProgram, String> {
158    let prog =
159        CString::new(program).map_err(|_| "unable to convert data to c string.".to_string())?;
160    Ok(JqProgram {
161        jq: jq::Jq::compile_program(prog).map_err(|e| {
162            // FIXME: string errors until v0.4
163            format!("{}", e)
164        })?,
165    })
166}
167
168#[cfg(test)]
169mod test {
170
171    use super::{compile, run};
172    use serde_json;
173
174    #[test]
175    fn reuse_compiled_program() {
176        let query = r#"if . == 0 then "zero" elif . == 1 then "one" else "many" end"#;
177        let mut prog = compile(&query).unwrap();
178        assert_eq!(prog.run("2").unwrap(), r#""many""#);
179        assert_eq!(prog.run("1").unwrap(), r#""one""#);
180        assert_eq!(prog.run("0").unwrap(), r#""zero""#);
181    }
182
183    #[test]
184    fn jq_state_is_not_global() {
185        let input = r#"{"id": 123, "name": "foo"}"#;
186        let query1 = r#".name"#;
187        let query2 = r#".id"#;
188
189        // Basically this test is just to check that the state pointers returned by
190        // `jq::init()` are completely independent and don't share any global state.
191        let mut prog1 = compile(&query1).unwrap();
192        let mut prog2 = compile(&query2).unwrap();
193
194        assert_eq!(prog1.run(input).unwrap(), r#""foo""#);
195        assert_eq!(prog2.run(input).unwrap(), r#"123"#);
196        assert_eq!(prog1.run(input).unwrap(), r#""foo""#);
197        assert_eq!(prog2.run(input).unwrap(), r#"123"#);
198    }
199
200    fn get_movies() -> serde_json::Value {
201        json!({
202            "movies": [
203                { "title": "Coraline", "year": 2009 },
204                { "title": "ParaNorman", "year": 2012 },
205                { "title": "Boxtrolls", "year": 2014 },
206                { "title": "Kubo and the Two Strings", "year": 2016 },
207                { "title": "Missing Link", "year": 2019 }
208            ]
209        })
210    }
211
212    #[test]
213    fn identity_nothing() {
214        assert_eq!(run(".", ""), Ok("".to_string()));
215    }
216
217    #[test]
218    fn identity_empty() {
219        assert_eq!(run(".", "{}"), Ok("{}".to_string()));
220    }
221
222    #[test]
223    fn extract_dates() {
224        let data = get_movies();
225        let query = "[.movies[].year]";
226        let output = run(query, &data.to_string()).unwrap();
227        let parsed: Vec<i64> = serde_json::from_str(&output).unwrap();
228        assert_eq!(vec![2009, 2012, 2014, 2016, 2019], parsed);
229    }
230
231    #[test]
232    fn extract_name() {
233        let res = run(".name", r#"{"name": "test"}"#);
234        assert_eq!(res, Ok(r#""test""#.to_string()));
235    }
236
237    #[test]
238    fn unpack_array() {
239        let res = run(".[]", "[1,2,3]");
240        assert_eq!(res, Ok("1\n2\n3".to_string()));
241    }
242
243    #[test]
244    fn compile_error() {
245        let res = run(". aa12312me  dsaafsdfsd", "{\"name\": \"test\"}");
246        assert!(res.is_err());
247    }
248
249    #[test]
250    fn parse_error() {
251        let res = run(".", "{1233 invalid json ahoy : est\"}");
252        assert!(res.is_err());
253    }
254
255    #[test]
256    fn just_open_brace() {
257        let res = run(".", "{");
258        assert!(res.is_err());
259    }
260
261    #[test]
262    fn just_close_brace() {
263        let res = run(".", "}");
264        assert!(res.is_err());
265    }
266
267    #[test]
268    fn total_garbage() {
269        let data = r#"
270        {
271            moreLike: "an object literal but also bad"
272            loveToDangleComma: true,
273        }"#;
274
275        let res = run(".", data);
276        assert!(res.is_err());
277    }
278
279    pub mod mem_errors {
280        //! Attempting run a program resulting in bad field access has been
281        //! shown to sometimes trigger a use after free or double free memory
282        //! error.
283        //!
284        //! Technically the program and inputs are both valid, but the
285        //! evaluation of the program causes bad memory access to happen.
286        //!
287        //! https://github.com/onelson/json-query/issues/4
288
289        use super::*;
290
291        #[test]
292        fn missing_field_access() {
293            let prog = ".[] | .hello";
294            let data = "[1,2,3]";
295            assert!(run(prog, data).is_err());
296        }
297
298        #[test]
299        fn missing_field_access_compiled() {
300            let mut prog = compile(".[] | .hello").unwrap();
301            let data = "[1,2,3]";
302            assert!(prog.run(data).is_err());
303        }
304    }
305}