use std::fmt::Display;
use std::process;
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap_builder::parser::ValuesRef;
use clap_complete::Shell;
use crate::error::{MyError, MyResult};
pub enum ShellKind {
Bash,
PowerShell,
}
pub struct Config {
pub paths: Vec<String>,
pub values: Vec<String>,
pub import: Option<String>,
pub sum: bool,
pub completion: Option<ShellKind>,
}
const PATHS_SHORT: &'static str = "Read values from text files (batch mode)";
const VALUES_SHORT: &'static str = "Read values from command line (batch mode)";
const IMPORT_SHORT: &'static str = "Import values from text file";
const SUM_SHORT: &'static str = "Sum values (batch mode)";
const SHELL_SHORT: &'static str = "Create completion script";
const PATHS_LONG: &'static str = "\
Read values and operations from text files or stdin in batch mode";
const VALUES_LONG: &'static str = "\
Read values and operations from command line in batch mode";
const IMPORT_LONG: &'static str = "\
Import values and operations from text file";
const SUM_LONG: &'static str = "\
Sum values if running in batch mode";
const SHELL_LONG: &'static str = "\
Create completion script:
Use \"--completion bash\" to create script for Bash
Use \"--completion ps\" to create script for PowerShell";
impl Config {
pub fn new(name: String, args: Vec<String>) -> MyResult<Config> {
let mut command = Self::create_command(name.clone());
let matches = Self::create_matches(&mut command, args)?;
let config = Self::create_config(&mut command, matches)?;
if let Some(completion) = config.completion {
Self::create_completion(&mut command, name, completion);
process::exit(1);
}
return Ok(config);
}
fn create_command(name: String) -> Command {
let mut index = 0;
let command = Command::new(name)
.version(clap::crate_version!())
.about(clap::crate_description!())
.author(clap::crate_authors!());
let command = command.arg(Self::create_arg("paths", &mut index)
.action(ArgAction::Append)
.help(PATHS_SHORT)
.long_help(PATHS_LONG));
let command = command.arg(Self::create_arg("command", &mut index)
.long("command")
.short('c')
.action(ArgAction::Append)
.value_name("VALUE")
.num_args(1..)
.allow_negative_numbers(true)
.help(VALUES_SHORT)
.long_help(VALUES_LONG));
let command = command.arg(Self::create_arg("import", &mut index)
.long("import")
.action(ArgAction::Set)
.help(IMPORT_SHORT)
.long_help(IMPORT_LONG));
let command = command.arg(Self::create_arg("sum", &mut index)
.long("sum")
.action(ArgAction::SetTrue)
.help(SUM_SHORT)
.long_help(SUM_LONG));
let command = command.arg(Self::create_arg("completion", &mut index)
.long("completion")
.action(ArgAction::Set)
.value_name("SHELL")
.value_parser(["bash", "ps"])
.hide_possible_values(true)
.help(SHELL_SHORT)
.long_help(SHELL_LONG));
return command;
}
fn create_arg(name: &'static str, index: &mut usize) -> Arg {
*index += 1;
return Arg::new(name).display_order(*index);
}
fn create_matches(command: &mut Command, args: Vec<String>) -> clap::error::Result<ArgMatches> {
match command.try_get_matches_from_mut(args) {
Ok(found) => Ok(found),
Err(error) => match error.kind() {
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
let error = error.to_string();
let error = error.trim_end();
eprintln!("{error}");
process::exit(1);
},
_ => Err(error),
}
}
}
fn create_config(command: &mut Command, matches: ArgMatches) -> MyResult<Config> {
let paths = Self::parse_values(matches.get_many("paths"));
let values = Self::parse_values(matches.get_many("command"));
let import = Self::parse_value(matches.get_one("import"));
let sum = matches.get_flag("sum");
let completion = Self::parse_completion(command, matches.get_one("completion"))?;
let config = Self {
paths,
values,
import,
sum,
completion,
};
return Ok(config);
}
fn parse_value(value: Option<&String>) -> Option<String> {
value.map(String::to_string)
}
fn parse_values(values: Option<ValuesRef<String>>) -> Vec<String> {
values.unwrap_or_default().map(String::to_string).collect()
}
fn parse_completion(command: &mut Command, value: Option<&String>) -> MyResult<Option<ShellKind>> {
let value = value.map(String::as_ref);
match value {
Some("bash") => Ok(Some(ShellKind::Bash)),
Some("ps") => Ok(Some(ShellKind::PowerShell)),
Some(value) => Err(Self::make_error(command, "completion", value)),
None => Ok(None),
}
}
fn create_completion(command: &mut Command, name: String, value: ShellKind) {
let mut stdout = std::io::stdout();
let value = match value {
ShellKind::Bash => Shell::Bash,
ShellKind::PowerShell => Shell::PowerShell,
};
clap_complete::generate(value, command, name, &mut stdout);
}
fn make_error<T: Display>(command: &mut Command, option: &str, value: T) -> MyError {
let message = format!("Invalid {option} option: {value}");
let error = command.error(clap::error::ErrorKind::ValueValidation, message);
return MyError::Clap(error);
}
}
impl Default for Config {
fn default() -> Self {
Self {
paths: Vec::new(),
values: Vec::new(),
import: None,
sum: false,
completion: None,
}
}
}
#[cfg(test)]
mod tests {
use std::fmt::Display;
use crate::config::Config;
#[test]
fn test_paths_are_handled() {
let expected: Vec<String> = vec![];
let args = vec!["rpn"];
let config = create_config(args);
assert_eq!(expected, config.paths);
let expected = vec!["file1"];
let args = vec!["rpn", "file1"];
let config = create_config(args);
assert_eq!(expected, config.paths);
let expected = vec!["file1", "file2"];
let args = vec!["rpn", "file1", "file2"];
let config = create_config(args);
assert_eq!(expected, config.paths);
}
#[test]
fn test_values_are_handled() {
let expected: Vec<String> = vec![];
let args = vec!["rpn"];
let config = create_config(args);
assert_eq!(expected, config.values);
let expected = vec!["1"];
let args = vec!["rpn", "-c", "1"];
let config = create_config(args);
assert_eq!(expected, config.values);
let expected = vec!["1", "-2", "add"];
let args = vec!["rpn", "--command", "1", "-2", "add"];
let config = create_config(args);
assert_eq!(expected, config.values);
}
#[test]
fn test_import_is_handled() {
let args = vec!["rpn"];
let config = create_config(args);
assert_eq!(None, config.import);
let args = vec!["rpn", "--import", "file"];
let config = create_config(args);
assert_eq!(Some(String::from("file")), config.import);
}
#[test]
fn test_sum_is_handled() {
let args = vec!["rpn"];
let config = create_config(args);
assert_eq!(false, config.sum);
let args = vec!["rpn", "--sum"];
let config = create_config(args);
assert_eq!(true, config.sum);
}
fn create_config(args: Vec<&str>) -> Config {
let mut command = Config::create_command(String::from("rpn"));
let args = args.into_iter().map(String::from).collect();
let matches = Config::create_matches(&mut command, args).unwrap_or_else(handle_error);
let config = Config::create_config(&mut command, matches).unwrap_or_else(handle_error);
return config;
}
fn handle_error<T, E: Display>(err: E)-> T {
panic!("{}", err);
}
}