lockspot 0.0.5

let's ask package-lock some questions
Documentation
extern crate atty;
extern crate clap;
extern crate regex;

use clap::{App, Arg, ArgMatches, SubCommand};
use regex::Regex;
use std::error::Error;
use std::{env, fs, io, process};

fn check_lifecycle() {
    let arg_string = env::args()
        .skip(1)
        .fold(String::new(), |mut arg_string, arg| {
            arg_string.push_str(" ");
            arg_string.push_str(&arg);
            arg_string
        });

    let install_lifecycles = ["install".to_owned(), "postinstall".to_owned()];
    match env::var("npm_lifecycle_event") {
        Ok(value) => {
            if install_lifecycles.contains(&value) {
                println!(
                    "
Hey! it looks like you're running lockspot in the \"postinstall\" or \"install\"
hook! this can create some confusion, because the package-lock may not have been
generated yet!
to prevent this, you can use the \"postshrinkwrap\" lifecycle hook instead, like
this:
\"scripts\": {{
    \"postshrinkwrap\": \"lockspot{}\"
}}
See this issue for further info: https://github.com/npm/npm/issues/18798",
                    arg_string
                );
            }
        }
        _ => {}
    }
}

fn get_lockfile(filename: &str) -> Result<lockspot::PackageLock, Box<dyn Error>> {
    let piping = !atty::is(atty::Stream::Stdin);
    let lock: lockspot::PackageLock = if piping || filename == "-" {
        serde_json::from_reader(io::stdin())?
    } else {
        let file = fs::File::open(filename)?;
        serde_json::from_reader(file)?
    };
    return Ok(lock);
}

fn try_regex(regex: &str) -> Option<Regex> {
    match Regex::new(regex) {
        Ok(r) => Some(r),
        _ => None,
    }
}

fn parse_flat_options(args: ArgMatches) -> lockspot::Options {
    if args.is_present("origami") {
        let origami_regex = Regex::new(r"@financial-times/o-.*").unwrap();
        lockspot::Options {
            patterns: vec![Some(origami_regex)],
            production: true,
            min: None,
            sort: None,
        }
    } else {
        let patterns: lockspot::Patterns = match args.values_of("pattern") {
            Some(pattern) => pattern.map(try_regex).collect::<lockspot::Patterns>(),
            None => vec![],
        };

        let production = args.is_present("production");

        lockspot::Options {
            patterns,
            production,
            min: None,
            sort: None,
        }
    }
}

fn flat(args: ArgMatches) -> Result<(), Box<dyn Error>> {
    let filename = match args.value_of("file") {
        Some(name) => name,
        None => "package-lock.json",
    };

    let options = parse_flat_options(args.to_owned());

    let lock = get_lockfile(filename)?;

    if !lockspot::validate(&lock) {
        panic!("not a valid lockfile! (only lockfile with `version: 1` is supported)");
    }

    if !lockspot::check_for_flatness(lock, options) {
        process::exit(1)
    } else {
        process::exit(0)
    }
}

fn parse_depcount_options(args: ArgMatches) -> lockspot::Options {
    let patterns: lockspot::Patterns = match args.values_of("pattern") {
        Some(pattern) => pattern.map(try_regex).collect::<lockspot::Patterns>(),
        None => vec![],
    };

    let sort: lockspot::SortType = match args.value_of("sort") {
        Some("dont") => lockspot::SortType::Dont,
        Some("name") => lockspot::SortType::Name,
        Some("count") => lockspot::SortType::Count,
        Some(_) => panic!("got an invalid sort, somehow"),
        None => lockspot::SortType::Dont,
    };

    let min: Option<usize> = match args.value_of("min") {
        Some(number) => match number.parse::<usize>() {
            Ok(number) => Some(number),
            Err(_) => None,
        },
        None => None,
    };

    let production = args.is_present("production");

    lockspot::Options {
        patterns,
        production,
        min,
        sort: Some(sort),
    }
}

fn depcount(args: ArgMatches) -> Result<(), Box<dyn Error>> {
    let filename = match args.value_of("file") {
        Some(name) => name,
        None => "package-lock.json",
    };

    let options = parse_depcount_options(args.to_owned());

    let lock = get_lockfile(filename)?;

    if !lockspot::validate(&lock) {
        panic!("not a valid lockfile! (only lockfile with `version: 1` is supported)");
    }

    println!("{}", lockspot::get_depcount(lock, options));

    Ok(())
}

fn get_args() -> clap::App<'static, 'static> {
    App::new("lockspot")
        .version("0.0.0")
        .author("chee <chee@snoot.club>")
        .about("let's ask package-lock")
        .subcommands(vec![
            SubCommand::with_name("flat")
                .about("exit badly if the tree cannot be flat")
                .args(&vec![
                    Arg::with_name("origami").long("origami"),
                    Arg::with_name("pattern")
                        .help("only count packages whose name match one of these patterns")
                        .long("pattern")
                        .short("r")
                        .multiple(true)
                        .takes_value(true),
                    Arg::with_name("production")
                        .help("only count the production (non-dev) tree")
                        .long("production")
                        .short("p")
                        .alias("prod"),
                     Arg::with_name("file")
					 .help("the package-lock.json to operate on")
					 .long("file")
					 .takes_value(true)
                ])])
        .subcommands(vec![
            SubCommand::with_name("depcount")
                .about("count the number of different versions of each dependency in the tree")
                .args(&vec![
                    Arg::with_name("min")
                        .help("only print dependencies that have more than this number of versions in the tree")
                        .long("min")
                        .default_value("1")
                        .takes_value(true),
                    Arg::with_name("production")
                        .help("only count the production (non-dev) tree")
                        .long("production")
                        .short("p")
                        .alias("prod"),
                    Arg::with_name("pattern")
                        .help("only count packages whose name match one of these patterns")
                        .long("pattern")
                        .short("r")
                        .multiple(true)
                        .takes_value(true),
                    Arg::with_name("sort")
                        .help("how to sort the dependencies. `dont` is the default.")
                        .long("sort")
                        .possible_values(&["dont", "count", "name"])
                        .takes_value(true)
					 .default_value("dont"),
				 Arg::with_name("file")
					 .help("the package-lock.json to operate on")
					 .long("file")
					 .takes_value(true)
                ])
        ])
}

fn main() -> Result<(), Box<dyn Error>> {
    check_lifecycle();

    let args = get_args().get_matches();

    if let Some(subcommand) = args.subcommand {
        let name = subcommand.name.as_str();

        match name {
            "depcount" => depcount(subcommand.matches)?,
            "flat" => flat(subcommand.matches)?,
            _ => {}
        }
    } else {
        get_args().print_help()?
    }

    Ok(())
}