dts_core/
jq.rs

1//! A wrapper for `jq`.
2
3use crate::{Error, Result};
4use serde_json::Value;
5use std::io::{self, BufRead, Write};
6use std::path::{Path, PathBuf};
7use std::process::{Child, Command, Stdio};
8use std::thread;
9
10/// A wrapper for the `jq` command.
11///
12/// This can be used to transform a `Value` using a `jq` expression.
13///
14/// ## Example
15///
16/// ```
17/// use dts_core::jq::Jq;
18/// use serde_json::json;
19/// # use std::error::Error;
20/// #
21/// # fn main() -> Result<(), Box<dyn Error>> {
22/// let value = json!([5, 4, 10]);
23///
24/// let jq = Jq::new()?;
25/// let result = jq.process("map(select(. > 5))", &value)?;
26///
27/// assert_eq!(result, json!([10]));
28/// #   Ok(())
29/// # }
30/// ```
31#[derive(Debug, Clone)]
32pub struct Jq {
33    executable: PathBuf,
34}
35
36impl Jq {
37    /// Creates a new `Jq` instance.
38    ///
39    /// ## Errors
40    ///
41    /// If the `jq` executable cannot be found in the `PATH` or is invalid an error is returned.
42    pub fn new() -> Result<Jq> {
43        Jq::with_executable("jq")
44    }
45
46    /// Creates a new `Jq` instance using the provided executable.
47    ///
48    /// ## Errors
49    ///
50    /// If `executable` cannot be found in `PATH`, does not exist (if absolute) or is invalid an
51    /// error is returned.
52    pub fn with_executable<P>(executable: P) -> Result<Jq>
53    where
54        P: AsRef<Path>,
55    {
56        let executable = executable.as_ref();
57
58        let output = Command::new(executable)
59            .arg("--version")
60            .output()
61            .map_err(|err| {
62                if let io::ErrorKind::NotFound = err.kind() {
63                    Error::new(format!("executable `{}` not found", executable.display()))
64                } else {
65                    Error::Io(err)
66                }
67            })?;
68
69        let executable = executable.to_path_buf();
70        let version = String::from_utf8_lossy(&output.stdout);
71
72        if version.starts_with("jq-") {
73            Ok(Jq { executable })
74        } else {
75            Err(Error::new(format!(
76                "executable `{}` exists but does appear to be `jq`",
77                executable.display()
78            )))
79        }
80    }
81
82    /// Processes a `Value` using the provided jq expression and returns the result.
83    ///
84    /// ## Errors
85    ///
86    /// - `Error::Io` if spawning `jq` fails or if there are other I/O errors.
87    /// - `Error::Json` if the data returned by `jq` cannot be deserialized.
88    /// - `Error::Message` on any other error.
89    pub fn process(&self, expr: &str, value: &Value) -> Result<Value> {
90        let mut cmd = self.spawn_cmd(expr)?;
91        let mut stdin = cmd.stdin.take().unwrap();
92
93        let buf = serde_json::to_vec(value)?;
94
95        thread::spawn(move || stdin.write_all(&buf));
96
97        let output = cmd.wait_with_output()?;
98
99        if output.status.success() {
100            process_output(&output.stdout)
101        } else {
102            Err(Error::new(String::from_utf8_lossy(&output.stderr)))
103        }
104    }
105
106    fn spawn_cmd(&self, expr: &str) -> io::Result<Child> {
107        Command::new(&self.executable)
108            .arg("--compact-output")
109            .arg("--monochrome-output")
110            .arg(expr)
111            .stdin(Stdio::piped())
112            .stdout(Stdio::piped())
113            .stderr(Stdio::piped())
114            .spawn()
115    }
116}
117
118fn process_output(buf: &[u8]) -> Result<Value, Error> {
119    let mut values = buf
120        .lines()
121        .map(|line| serde_json::from_str(&line.unwrap()))
122        .collect::<Result<Vec<Value>, _>>()?;
123
124    if values.len() == 1 {
125        Ok(values.remove(0))
126    } else {
127        Ok(Value::Array(values))
128    }
129}