#![doc = include_str!("../README.md")]
mod args;
mod panic;
#[cfg(not(target_arch = "loongarch64"))]
use mimalloc::MiMalloc;
#[cfg(not(target_arch = "loongarch64"))]
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
use std::{
fs,
io::{
BufRead,
Read,
Write,
stdin,
stdout,
},
path::Path,
process::exit,
};
use anyhow::{
Context,
Result,
anyhow,
};
use args::Args;
use clap::Parser;
use colored_json::{
ColoredFormatter,
CompactFormatter,
PrettyFormatter,
};
use jql_runner::runner;
use panic::use_custom_panic_hook;
use serde::Deserialize;
use serde_json::Value;
use serde_stacker::Deserializer;
fn read_file(path: impl AsRef<Path>) -> Result<String> {
let display_path = path.as_ref().display();
let contents =
fs::read(&path).with_context(|| format!("Failed to read from file {display_path}"))?;
Ok(String::from_utf8_lossy(&contents).into_owned())
}
fn render(result: Result<String>) {
match result {
Ok(output) => println!("{output}"),
Err(error) => {
eprintln!("{error}");
exit(1);
}
}
}
fn process_json(json: &str, args: &Args) -> Result<String> {
if args.validate {
return serde_json::from_str::<Value>(json).map_or_else(
|_| Err(anyhow!("Invalid JSON file or content")),
|_| Ok("Valid JSON file or content".to_string()),
);
}
let query = match args.query_from_file.as_deref() {
Some(path) => read_file(path)?,
None => args.query.as_deref().unwrap().to_string(),
};
let mut deserializer = serde_json::Deserializer::from_str(json);
deserializer.disable_recursion_limit();
let deserializer = Deserializer::new(&mut deserializer);
let value: Value = Value::deserialize(deserializer)
.with_context(|| "Failed to deserialize the JSON data".to_string())?;
let mut result: Value = runner::raw(&query, &value)?;
if args.sort_keys {
result.sort_all_objects();
}
if args.inline {
return ColoredFormatter::new(CompactFormatter {})
.to_colored_json_auto(&result)
.with_context(|| "Failed to inline the JSON data".to_string());
}
if args.raw_string && result.is_string() {
return Ok(String::from(result.as_str().unwrap()));
}
ColoredFormatter::new(PrettyFormatter::new())
.to_colored_json_auto(&result)
.with_context(|| "Failed to format the JSON data".to_string())
}
fn main() -> Result<()> {
use_custom_panic_hook();
let args = Args::parse();
if let Some(path) = args.json_file.as_deref() {
let contents = read_file(path)?;
render(process_json(&contents, &args));
return Ok(());
}
if args.stream {
let mut stdout = stdout().lock();
for line in stdin().lock().lines() {
let line = line.with_context(|| "Failed to read stream".to_string())?;
render(process_json(&line, &args));
stdout
.flush()
.with_context(|| "Failed to flush stdout".to_string())?;
}
return Ok(());
}
let mut buffer = Vec::new();
stdin()
.lock()
.read_to_end(&mut buffer)
.with_context(|| "Failed to read piped content from stdin".to_string())?;
let lines = String::from_utf8(buffer)
.with_context(|| "Failed to convert piped content from stdin".to_string())?;
render(process_json(&lines, &args));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sort_keys_flag_sorts_objects_recursively() {
let json = r#"{ "root": { "d": 1, "b": { "y": 1, "x": 2 }, "a": 3 } }"#;
let args = Args::parse_from(["jql", "--sort-keys", "--inline", r#""root""#]);
assert_eq!(
process_json(json, &args).unwrap(),
r#"{"a":3,"b":{"x":2,"y":1},"d":1}"#
);
}
#[test]
fn output_keeps_source_order_without_the_flag() {
let json = r#"{ "root": { "d": 1, "a": 3 } }"#;
let args = Args::parse_from(["jql", "--inline", r#""root""#]);
assert_eq!(process_json(json, &args).unwrap(), r#"{"d":1,"a":3}"#);
}
}