automaat_processor_json_edit/
lib.rs

1//! An [Automaat] processor to run a `jq` program against a JSON string.
2//!
3//! You can use this processor to manipulate JSON strings provided by another
4//! processor.
5//!
6//! This is a very powerful and versatile processor, courtesy of the [`jq`]
7//! library. It allows you do filter data, manipulate data, or return boolean
8//! values that can be used in the next processor to decide its output.
9//!
10//! [Automaat]: automaat_core
11//! [`jq`]: https://stedolan.github.io/jq/manual/v1.6/
12//!
13//! # Example
14//!
15//! Take the value of the `hello` key, and uppercase the ASCII characters.
16//!
17//! ```rust
18//! # fn main() -> Result<(), Box<std::error::Error>> {
19//! use automaat_core::{Context, Processor};
20//! use automaat_processor_json_edit::JsonEdit;
21//!
22//! let context = Context::new()?;
23//!
24//! let processor = JsonEdit {
25//!     json: r#"{"hello":"world"}"#.to_owned(),
26//!     program: ".hello | ascii_upcase".to_owned(),
27//!     pretty_output: false,
28//! };
29//!
30//! let output = processor.run(&context)?;
31//!
32//! assert_eq!(output, Some("WORLD".to_owned()));
33//! #     Ok(())
34//! # }
35//! ```
36//!
37//! # Package Features
38//!
39//! * `juniper` – creates a set of objects to be used in GraphQL-based
40//!   requests/responses.
41#![deny(
42    clippy::all,
43    clippy::cargo,
44    clippy::nursery,
45    clippy::pedantic,
46    deprecated_in_future,
47    future_incompatible,
48    missing_docs,
49    nonstandard_style,
50    rust_2018_idioms,
51    rustdoc,
52    warnings,
53    unused_results,
54    unused_qualifications,
55    unused_lifetimes,
56    unused_import_braces,
57    unsafe_code,
58    unreachable_pub,
59    trivial_casts,
60    trivial_numeric_casts,
61    missing_debug_implementations,
62    missing_copy_implementations
63)]
64#![warn(variant_size_differences)]
65#![allow(clippy::multiple_crate_versions, missing_doc_code_examples)]
66#![doc(html_root_url = "https://docs.rs/automaat-processor-json-edit/0.1.0")]
67
68use automaat_core::{Context, Processor};
69use serde::{Deserialize, Serialize};
70use serde_json::Value;
71use std::{error, fmt};
72
73/// The processor configuration.
74#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
75#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
76pub struct JsonEdit {
77    /// The JSON string that will be parsed by the `program` value.
78    pub json: String,
79
80    /// The program to run against the provided `json` string.
81    ///
82    /// A program can either filter the JSON down to a subset of data, or can
83    /// mutate the data before returning a value.
84    ///
85    /// To learn about the supported syntax, see the `jq` documentation:
86    ///
87    /// https://stedolan.github.io/jq/manual/v1.6/
88    pub program: String,
89
90    /// "Pretty print" the JSON output.
91    ///
92    /// If set to false, the JSON will be printed in a compact format, without
93    /// any indentation, spacing or newlines.
94    pub pretty_output: bool,
95}
96
97/// The GraphQL [Input Object][io] used to initialize the processor via an API.
98///
99/// [`JsonEdit`] implements `From<Input>`, so you can directly initialize the
100/// processor using this type.
101///
102/// _requires the `juniper` package feature to be enabled_
103///
104/// [io]: https://graphql.github.io/graphql-spec/June2018/#sec-Input-Objects
105#[cfg(feature = "juniper")]
106#[graphql(name = "StringRegexInput")]
107#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, juniper::GraphQLInputObject)]
108pub struct Input {
109    json: String,
110    program: String,
111    pretty_output: Option<bool>,
112}
113
114#[cfg(feature = "juniper")]
115impl From<Input> for JsonEdit {
116    fn from(input: Input) -> Self {
117        Self {
118            json: input.json,
119            program: input.program,
120            pretty_output: input.pretty_output.unwrap_or(false),
121        }
122    }
123}
124
125impl JsonEdit {
126    fn to_string(&self, value: &Value) -> Result<String, serde_json::Error> {
127        if value.is_string() {
128            return Ok(value.as_str().unwrap().to_owned());
129        };
130
131        if self.pretty_output {
132            serde_json::to_string_pretty(&value)
133        } else {
134            serde_json::to_string(&value)
135        }
136    }
137}
138
139impl<'a> Processor<'a> for JsonEdit {
140    const NAME: &'static str = "JSON Edit";
141
142    type Error = Error;
143    type Output = String;
144
145    /// Validate that the provided `program` syntax is valid.
146    ///
147    /// # Errors
148    ///
149    /// If the program contains invalid syntax, the [`Error::Json`] error
150    /// variant is returned.
151    fn validate(&self) -> Result<(), Self::Error> {
152        json_query::compile(self.program.as_str())
153            .map(|_| ())
154            .map_err(Into::into)
155    }
156
157    /// Run the provided `program` against the `json` data.
158    ///
159    /// # Output
160    ///
161    /// If the final output is a string, the surrounding JSON quotes are
162    /// removed. This makes it easier to show raw strings in the UI, without
163    /// having to use the regex processor to remove extra quotes.
164    ///
165    /// This output:
166    ///
167    /// ```json
168    /// "world"
169    /// ```
170    ///
171    /// Becomes this:
172    ///
173    /// ```json
174    /// world
175    /// ```
176    ///
177    /// If `pretty_output` is set, any JSON object or array is pretty printed,
178    /// by including newlines, indentation and spacing around the key/value
179    /// pairs.
180    ///
181    /// This output:
182    ///
183    /// ```json
184    /// {"hello":"world"}
185    /// ```
186    ///
187    /// Becomes this:
188    ///
189    /// ```json
190    /// {
191    ///   "hello": "world"
192    /// }
193    /// ```
194    ///
195    /// When unwrapping arrays in the program, each line is processed according
196    /// to the above rules.
197    ///
198    /// So this output:
199    ///
200    /// ```json
201    /// [{"hello":"world"},"hello",2]
202    /// ```
203    ///
204    /// Becomes this:
205    ///
206    /// ```json
207    /// {"hello":"world"}
208    /// hello
209    /// 2
210    /// ```
211    ///
212    /// When using the program `.[]`.
213    ///
214    /// # Errors
215    ///
216    /// This method returns the [`Error::Json`] error variant if the provided
217    /// `json` input or the `program` has invalid syntax.
218    ///
219    /// The [`Error::Serde`] error variant is returned if the processor failed
220    /// to serialize or deserialize the input/output JSON.
221    fn run(&self, _context: &Context) -> Result<Option<Self::Output>, Self::Error> {
222        let mut output = vec![];
223        let json = json_query::run(self.program.as_str(), self.json.as_str())?;
224
225        // The jq program can return multiple lines of JSON if an array is
226        // unpacked.
227        for line in json.lines() {
228            let value: Value = serde_json::from_str(line)?;
229
230            if !value.is_null() {
231                output.push(self.to_string(&value)?)
232            }
233        }
234
235        let string = output.join("\n").trim().to_owned();
236
237        if string.is_empty() {
238            Ok(None)
239        } else {
240            Ok(Some(string))
241        }
242    }
243}
244
245/// Represents all the ways that [`JsonEdit`] can fail.
246///
247/// This type is not intended to be exhaustively matched, and new variants may
248/// be added in the future without a major version bump.
249#[derive(Debug)]
250pub enum Error {
251    /// A syntax error.
252    Json(String),
253
254    /// An error during serialization or deserialization.
255    Serde(serde_json::Error),
256
257    #[doc(hidden)]
258    __Unknown, // Match against _ instead, more variants may be added in the future.
259}
260
261impl fmt::Display for Error {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        match *self {
264            Error::Json(ref err) => write!(f, "JSON error: {}", err),
265            Error::Serde(ref err) => write!(f, "Serde error: {}", err),
266            Error::__Unknown => unreachable!(),
267        }
268    }
269}
270
271impl error::Error for Error {
272    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
273        match *self {
274            Error::Json(_) => None,
275            Error::Serde(ref err) => Some(err),
276            Error::__Unknown => unreachable!(),
277        }
278    }
279}
280
281impl From<String> for Error {
282    fn from(err: String) -> Self {
283        Error::Json(err)
284    }
285}
286
287impl From<serde_json::Error> for Error {
288    fn from(err: serde_json::Error) -> Self {
289        Error::Serde(err)
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    fn processor_stub() -> JsonEdit {
298        JsonEdit {
299            json: r#"{"hello":"world"}"#.to_owned(),
300            program: ".hello".to_owned(),
301            pretty_output: false,
302        }
303    }
304
305    mod run {
306        use super::*;
307
308        #[test]
309        fn test_mismatch_program() {
310            let mut processor = processor_stub();
311            processor.json = r#"{"hello":"world"}"#.to_owned();
312            processor.program = ".hi".to_owned();
313
314            let context = Context::new().unwrap();
315            let output = processor.run(&context).unwrap();
316
317            assert!(output.is_none())
318        }
319
320        #[test]
321        fn test_match_program() {
322            let mut processor = processor_stub();
323            processor.json = r#"{"hello":"world"}"#.to_owned();
324            processor.program = ".hello".to_owned();
325
326            let context = Context::new().unwrap();
327            let output = processor.run(&context).unwrap().expect("Some");
328
329            assert_eq!(output, "world".to_owned())
330        }
331
332        #[test]
333        fn test_unwrapped_array() {
334            let mut processor = processor_stub();
335            processor.json = r#"[{"hello":"world"},{"hello":2}]"#.to_owned();
336            processor.program = ".[] | .hello".to_owned();
337
338            let context = Context::new().unwrap();
339            let output = processor.run(&context).unwrap().expect("Some");
340
341            assert_eq!(output, "world\n2".to_owned())
342        }
343
344        #[test]
345        fn test_empty_output() {
346            let mut processor = processor_stub();
347            processor.json = r#"[{"hello":"world"},{"hello":2}]"#.to_owned();
348            processor.program = ".[0]".to_owned();
349            processor.pretty_output = true;
350
351            let context = Context::new().unwrap();
352            let output = processor.run(&context).unwrap().expect("Some");
353
354            assert_eq!(output, "{\n  \"hello\": \"world\"\n}".to_owned())
355        }
356
357        #[test]
358        fn test_combination_of_empty_and_non_empty_lines() {
359            let mut processor = processor_stub();
360            processor.json = r#"["","hello","","world"]"#.to_owned();
361            processor.program = ".[]".to_owned();
362            processor.pretty_output = true;
363
364            let context = Context::new().unwrap();
365            let output = processor.run(&context).unwrap().expect("Some");
366
367            // the double newline is as expected, since we trim the start and
368            // the end of the output, but keep any newlines you need in the
369            // middle of the output. We do still remove `null` values in the
370            // middle, to allow for different behaviors depending on the need.
371            assert_eq!(output, "hello\n\nworld".to_owned())
372        }
373
374        #[test]
375        fn test_combination_of_null_and_non_null_lines() {
376            let mut processor = processor_stub();
377            processor.json = r#"[null,"hello",null,"world"]"#.to_owned();
378            processor.program = ".[]".to_owned();
379            processor.pretty_output = true;
380
381            let context = Context::new().unwrap();
382            let output = processor.run(&context).unwrap().expect("Some");
383
384            assert_eq!(output, "hello\nworld".to_owned())
385        }
386
387        #[test]
388        fn test_empty_string_output() {
389            let mut processor = processor_stub();
390            processor.json = r#"["",""]"#.to_owned();
391            processor.program = ".[]".to_owned();
392            processor.pretty_output = true;
393
394            let context = Context::new().unwrap();
395            let output = processor.run(&context).unwrap();
396
397            assert!(output.is_none())
398        }
399
400        #[test]
401        fn test_null_output() {
402            let mut processor = processor_stub();
403            processor.json = r#"{"hello":"world"}"#.to_owned();
404            processor.program = ".hi".to_owned();
405            processor.pretty_output = true;
406
407            let context = Context::new().unwrap();
408            let output = processor.run(&context).unwrap();
409
410            assert!(output.is_none())
411        }
412
413        #[test]
414        fn test_pretty_output_multi_line() {
415            let mut processor = processor_stub();
416            processor.json = r#"[{"hello":"world"},{"hello":2}]"#.to_owned();
417            processor.program = ".[]".to_owned();
418            processor.pretty_output = true;
419
420            let context = Context::new().unwrap();
421            let output = processor.run(&context).unwrap().expect("Some");
422
423            assert_eq!(
424                output,
425                "{\n  \"hello\": \"world\"\n}\n{\n  \"hello\": 2\n}".to_owned()
426            )
427        }
428    }
429
430    mod validate {
431        use super::*;
432
433        #[test]
434        fn test_valid_syntax() {
435            let mut processor = processor_stub();
436            processor.program = r".hello".to_owned();
437
438            processor.validate().unwrap()
439        }
440
441        #[test]
442        #[should_panic]
443        fn test_invalid_syntax() {
444            let mut processor = processor_stub();
445            processor.program = r"..hello \NO".to_owned();
446
447            processor.validate().unwrap()
448        }
449    }
450
451    #[test]
452    fn test_readme_deps() {
453        version_sync::assert_markdown_deps_updated!("README.md");
454    }
455
456    #[test]
457    fn test_html_root_url() {
458        version_sync::assert_html_root_url_updated!("src/lib.rs");
459    }
460}