#[macro_use]
extern crate clap;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process;
use clap::{App, Arg};
use console::Term;
use rayon::prelude::*;
use walkdir::{DirEntry, WalkDir};
use json_structural_diff::{colorize, JsonDiff};
struct Config {
raw: bool,
only_keys: bool,
color: bool,
}
fn act_on_file(
path1: &PathBuf,
path2: &PathBuf,
output_path: Option<&PathBuf>,
cfg: &Config,
) -> std::io::Result<()> {
let buffer1 = std::fs::read(path1).unwrap();
let buffer2 = std::fs::read(path2).unwrap();
if let (Ok(json1), Ok(json2)) = (
serde_json::from_slice(&buffer1),
serde_json::from_slice(&buffer2),
) {
if json1 != json2 {
let json_diff = JsonDiff::diff(&json1, &json2, cfg.only_keys);
let result = json_diff.diff.unwrap();
let json_string = if cfg.raw {
serde_json::to_string_pretty(&result)?
} else {
colorize(&result, cfg.color)
};
if let Some(output_path) = output_path {
let output_filename = path1.file_name().unwrap().to_str().unwrap();
let mut output_file = File::create(output_path.join(output_filename))?;
writeln!(&mut output_file, "{json_string}")?;
} else {
let mut term = Term::stdout();
term.write_all(json_string.as_bytes())?;
}
}
}
Ok(())
}
fn is_hidden(entry: &DirEntry) -> bool {
entry
.file_name()
.to_str()
.is_some_and(|s| s.starts_with('.'))
}
fn explore(path1: &PathBuf, path2: &PathBuf, output_path: Option<&PathBuf>, cfg: &Config) {
WalkDir::new(path1)
.into_iter()
.filter_entry(|e| !is_hidden(e))
.zip(
WalkDir::new(path2)
.into_iter()
.filter_entry(|e| !is_hidden(e)),
)
.par_bridge()
.for_each(|(entry1, entry2)| {
let entry1 = entry1.as_ref().unwrap();
let path1_file: PathBuf = entry1.path().to_path_buf();
let entry2 = entry2.as_ref().unwrap();
let path2_file: PathBuf = entry2.path().to_path_buf();
if path1_file.is_file()
&& path2_file.is_file()
&& path1_file.extension().unwrap() == "json"
&& path2_file.extension().unwrap() == "json"
{
act_on_file(&path1_file, &path2_file, output_path, cfg).unwrap();
}
});
}
fn exist_or_exit(path: &Path, which_path: &str) {
if !(path.exists()) {
eprintln!(
"The {} path `{}` is not correct",
which_path,
path.to_str().unwrap()
);
process::exit(1);
}
}
fn main() {
let matches = App::new("json-diff")
.version(crate_version!())
.author(&*env!("CARGO_PKG_AUTHORS").replace(':', "\n"))
.about("Find the differences between two input json files")
.arg(
Arg::with_name("color")
.help("Colored output")
.short("c")
.long("--[no-]color"),
)
.arg(
Arg::with_name("raw")
.help("Display raw JSON encoding of the diff")
.short("j")
.long("raw-json"),
)
.arg(
Arg::with_name("keys")
.help("Compare only the keys, ignore the differences in values")
.short("k")
.long("keys-only"),
)
.arg(
Arg::with_name("output")
.help("Output directory")
.short("o")
.long("output")
.takes_value(true),
)
.arg(
Arg::with_name("first-json")
.help("Old json file")
.required(true)
.takes_value(true),
)
.arg(
Arg::with_name("second-json")
.help("New json file")
.required(true)
.takes_value(true),
)
.get_matches();
let path1 = PathBuf::from(matches.value_of("first-json").unwrap());
let path2 = PathBuf::from(matches.value_of("second-json").unwrap());
let output_path = if let Some(path) = matches.value_of("output") {
let path = PathBuf::from(path);
exist_or_exit(&path, "output");
Some(path)
} else {
None
};
exist_or_exit(&path1, "first");
exist_or_exit(&path2, "second");
let color = if output_path.is_none() {
matches.is_present("color")
} else {
false
};
let raw = matches.is_present("raw");
let only_keys = matches.is_present("keys");
let cfg = Config {
raw,
only_keys,
color,
};
if path1.is_dir() && path2.is_dir() {
explore(&path1, &path2, output_path.as_ref(), &cfg);
} else if (path1.is_dir() && !path2.is_dir()) || (!path1.is_dir() && path2.is_dir()) {
eprintln!("Both paths should be a directory or a file",);
process::exit(1);
} else {
act_on_file(&path1, &path2, output_path.as_ref(), &cfg).unwrap();
}
}