jset 0.1.0

a command-line tool for performing set operations on a list of json files
Documentation
use clap::{App, Arg, ArgMatches, SubCommand};
use std::io;

fn main() -> Result<(), JsetError> {
    let common_args = [
        Arg::with_name("paths")
            .index(1)
            .required(true)
            .min_values(2)
            .multiple(true),
        Arg::with_name("compact")
            .short("c")
            .help("compact instead of pretty-printed output"),
    ];

    let app_matches = App::new("jset")
        .about("a command-line tool for performing set operations on json files")
        .subcommand(
            SubCommand::with_name("intersect")
                .about("calculate the intersection of multiple json files")
                .args(&common_args),
        )
        .subcommand(
            SubCommand::with_name("union")
                .about("calculate the union of multiple json files")
                .args(&common_args),
        )
        .subcommand(
            SubCommand::with_name("difference")
                .visible_alias("diff")
                .about("calculate the difference of multiple json files. files 2..n are subtracted from the first file")
                .args(&common_args),
        )
        .get_matches();

    match app_matches.subcommand() {
        (name, Some(matches)) => {
            let cli = Cli::from(matches);
            match name {
                "intersect" => cli.execute(jset::intersect),
                "union" => cli.execute(jset::union),
                "difference" => cli.execute(jset::difference),
                name => panic!("unknown subcommand {}", name),
            }
        }
        (name, _) => panic!("unknown subcommand {}", name),
    }
}

#[derive(Debug)]
enum JsetError {
    NotFound,
    Generic(String),
}

impl From<io::Error> for JsetError {
    fn from(err: io::Error) -> Self {
        JsetError::Generic(err.to_string())
    }
}

impl From<serde_json::Error> for JsetError {
    fn from(err: serde_json::Error) -> Self {
        JsetError::Generic(err.to_string())
    }
}

struct Cli {
    paths: Vec<String>,
    compact: bool,
}

impl From<&ArgMatches<'_>> for Cli {
    fn from(matches: &ArgMatches) -> Self {
        Cli {
            paths: matches
                .values_of("paths")
                .unwrap()
                .map(|s| s.to_owned())
                .collect(),
            compact: matches.is_present("compact"),
        }
    }
}

impl Cli {
    fn execute<F>(&self, operation: F) -> Result<(), JsetError>
    where
        F: Fn(&serde_json::Value, &serde_json::Value) -> Option<serde_json::Value>,
    {
        let mut paths_iter = self.paths.iter();
        let mut acc = read_to_value(paths_iter.next().unwrap())?;
        for path in paths_iter {
            let v = read_to_value(path)?;
            acc = match operation(&acc, &v) {
                Some(iv) => iv,
                None => return Err(JsetError::NotFound),
            }
        }

        if self.compact {
            println!("{}", serde_json::to_string(&acc)?);
        } else {
            println!("{}", serde_json::to_string_pretty(&acc)?);
        }

        Ok(())
    }
}

fn read_to_value(path: &str) -> Result<serde_json::Value, JsetError> {
    let str = std::fs::read_to_string(path)?;
    Ok(serde_json::from_str::<serde_json::Value>(&str)?)
}