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}