neomake 0.2.9

Yet another task runner as make alternative, inspired by GitLab pipelines.
use std::{
    collections::HashMap,
    error::Error,
    iter::FromIterator,
    result::Result,
};

#[derive(Debug)]
pub struct CallArgs {
    pub experimental: bool,
    pub command: Command,
}

impl CallArgs {
    pub fn validate(&self) -> Result<(), Box<dyn Error>> {
        if self.experimental {
            return Ok(());
        }

        match &self.command {
            | Command::Describe { .. } => Err(Box::new(crate::error::ExperimentalCommandError::new(
                "command is experimental",
            ))),
            | Command::List { .. } => Err(Box::new(crate::error::ExperimentalCommandError::new(
                "command is experimental",
            ))),
            | _ => Ok(()),
        }
    }
}

#[derive(Debug)]
pub enum Format {
    JSON,
    YAML,
}

#[derive(Debug)]
pub enum Command {
    Init,
    Run {
        config: crate::config::Config,
        chains: Vec<String>,
        args: HashMap<String, String>,
    },
    List {
        config: crate::config::Config,
        format: Format,
    },
    Describe {
        config: crate::config::Config,
        chains: Vec<String>,
        format: Format,
    },
}

pub struct ClapArgumentLoader {}

impl ClapArgumentLoader {
    pub fn load() -> Result<CallArgs, Box<dyn Error>> {
        let command = clap::App::new("neomake")
            .version(env!("CARGO_PKG_VERSION"))
            .about("neomake")
            .author("replicadse <aw@voidpointergroup.com>")
            .arg(
                clap::Arg::new("experimental")
                    .short('e')
                    .long("experimental")
                    .value_name("EXPERIMENTAL")
                    .help("Enables experimental features that do not count as stable.")
                    .required(false)
                    .takes_value(false),
            )
            .subcommand(clap::App::new("init").about("Initializes a new default configuration in the current folder."))
            .subcommand(
                clap::App::new("run")
                    .about("Runs task chains.")
                    .visible_aliases(&["r", "exec", "x"])
                    .arg(
                        clap::Arg::new("config")
                            .short('f')
                            .long("config")
                            .value_name("CONFIG")
                            .help("The configuration file to use.")
                            .default_value("./.neomake.yaml")
                            .multiple_values(false)
                            .required(false)
                            .takes_value(true),
                    )
                    .arg(
                        clap::Arg::new("chain")
                            .short('c')
                            .long("chain")
                            .value_name("CHAIN")
                            .help("Which chain to execute.")
                            .multiple_occurrences(true)
                            .required(true)
                            .takes_value(true),
                    )
                    .arg(
                        clap::Arg::new("arg")
                            .short('a')
                            .long("arg")
                            .value_name("ARG")
                            .help("An argument to the chain.")
                            .multiple_values(true)
                            .required(false)
                            .takes_value(true),
                    ),
            )
            .subcommand(
                clap::App::new("describe")
                    .about("Describes the execution graph for a given task chain configuration.")
                    .visible_aliases(&["d", "desc"])
                    .arg(
                        clap::Arg::new("config")
                            .short('f')
                            .long("config")
                            .value_name("CONFIG")
                            .help("The configuration file to use.")
                            .default_value("./.neomake.yaml")
                            .multiple_values(false)
                            .required(false)
                            .takes_value(true),
                    )
                    .arg(
                        clap::Arg::new("chain")
                            .short('c')
                            .long("chain")
                            .value_name("CHAIN")
                            .help("Which chain to execute.")
                            .multiple_occurrences(true)
                            .required(true)
                            .takes_value(true),
                    )
                    .arg(
                        clap::Arg::new("output")
                            .short('o')
                            .long("output")
                            .value_name("OUTPUT")
                            .help("The output format.")
                            .default_value("yaml")
                            .possible_values(&["yaml", "json"])
                            .required(false)
                            .takes_value(true),
                    ),
            )
            .subcommand(
                clap::App::new("list")
                    .about("Lists all available task chains.")
                    .visible_aliases(&["ls"])
                    .arg(
                        clap::Arg::new("config")
                            .short('f')
                            .long("config")
                            .value_name("CONFIG")
                            .help("The configuration file to use.")
                            .default_value("./.neomake.yaml")
                            .multiple_values(false)
                            .required(false)
                            .takes_value(true),
                    )
                    .arg(
                        clap::Arg::new("output")
                            .short('o')
                            .long("output")
                            .value_name("OUTPUT")
                            .help("The output format.")
                            .default_value("yaml")
                            .possible_values(&["yaml", "json"])
                            .required(false)
                            .takes_value(true),
                    ),
            )
            .get_matches();

        fn parse_config(x: &clap::ArgMatches) -> Result<crate::config::Config, Box<dyn Error>> {
            let config_content = if x.is_present("config") {
                let config_param = x.value_of("config").unwrap();
                std::fs::read_to_string(config_param)?
            } else {
                return Err(Box::new(crate::error::MissingArgumentError::new(
                    "configuration has not been specified",
                )));
            };

            fn check_version(config: &str) -> Result<(), Box<dyn Error>> {
                #[derive(Debug, serde::Deserialize)]
                struct WithVersion {
                    version: String,
                }
                let v: WithVersion = serde_yaml::from_str(config)?;

                if v.version != "0.2" {
                    Err(Box::new(crate::error::VersionCompatibilityError::new(&format!(
                        "config version {} is incompatible with this CLI version",
                        v.version
                    ))))
                } else {
                    Ok(())
                }
            }
            check_version(&config_content)?;

            Ok(serde_yaml::from_str(&config_content)?)
        }

        fn parse_chains(x: &clap::ArgMatches) -> Result<Vec<String>, Box<dyn Error>> {
            let chains = x
                .values_of("chain")
                .ok_or(Box::new(crate::error::MissingArgumentError::new(
                    "chain was not specified",
                )))?;

            Ok(Vec::<String>::from_iter(chains.into_iter().map(|v| v.to_owned())))
        }

        let cmd = if let Some(..) = command.subcommand_matches("init") {
            Command::Init
        } else if let Some(x) = command.subcommand_matches("run") {
            let mut args_map: HashMap<String, String> = HashMap::new();
            if let Some(args) = x.values_of("arg") {
                for v_arg in args {
                    let spl: Vec<&str> = v_arg.splitn(2, "=").collect();
                    args_map.insert(spl[0].to_owned(), spl[1].to_owned());
                }
            }
            Command::Run {
                config: parse_config(x)?,
                chains: parse_chains(x)?,
                args: args_map,
            }
        } else if let Some(x) = command.subcommand_matches("list") {
            let format = if let Some(f) = x.value_of("output") {
                match f {
                    | "yaml" => Format::YAML,
                    | "json" => Format::JSON,
                    | _ => Err(Box::new(crate::error::ArgumentError::new("unkown output format")))?,
                }
            } else {
                Format::JSON
            };

            Command::List {
                config: parse_config(x)?,
                format,
            }
        } else if let Some(x) = command.subcommand_matches("describe") {
            let format = if let Some(f) = x.value_of("output") {
                match f {
                    | "yaml" => Format::YAML,
                    | "json" => Format::JSON,
                    | _ => Err(Box::new(crate::error::ArgumentError::new("unkown output format")))?,
                }
            } else {
                Format::JSON
            };

            Command::Describe {
                config: parse_config(x)?,
                chains: parse_chains(x)?,
                format,
            }
        } else {
            return Err(Box::new(crate::error::UnknownCommandError::new("unknown command")));
        };

        let callargs = CallArgs {
            experimental: command.contains_id("experimental"),
            command: cmd,
        };

        callargs.validate()?;
        Ok(callargs)
    }
}