use crate::error::CliError;
use std::collections::HashMap;
use std::fs;
use std::ops::Range;
use url::Url;
pub struct CliRequest {
pub cmd_alias: String,
pub is_help: bool,
pub args: Vec<String>,
pub flags: Vec<String>,
pub flag_values: HashMap<String, String>,
pub shortcuts: Vec<String>,
}
#[derive(Clone, PartialEq)]
pub enum CliFormat {
Any,
Integer,
Decimal,
Boolean,
Email,
Url,
StringRange(Range<usize>),
IntegerRange(Range<i64>),
DecimalRange(Range<f64>),
OneOf(Vec<String>),
File,
Directory,
}
impl CliRequest {
pub fn require_params(&self, num: usize) -> Result<(), CliError> {
match self.args.len() {
len if len >= num => Ok(()),
_ => Err(CliError::MissingParams),
}
}
pub fn require_flag(&self, flag: &str) -> Result<(), CliError> {
if self.has_flag(&flag) {
Ok(())
} else {
Err(CliError::MissingFlag(flag.to_string()))
}
}
pub fn get_flag(&self, flag: &str) -> Option<String> {
match self.flag_values.get(&flag.to_string()) {
Some(r) => Some(r.clone()),
None => None,
}
}
pub fn validate_flag(&self, flag: &str, format: CliFormat) -> Result<(), CliError> {
let value = self.get_flag(&flag).ok_or(CliError::MissingFlag(flag.to_string()))?;
self.validate(0, &value, format.clone())?;
Ok(())
}
pub fn has_flag(&self, flag: &str) -> bool {
self.flags.contains(&flag.to_string()) || self.flag_values.contains_key(&flag.to_string())
}
pub fn validate_params(&self, formats: Vec<CliFormat>) -> Result<(), CliError> {
for (pos, format) in formats.iter().enumerate() {
let arg = self.args.get(pos).ok_or_else(|| {
CliError::InvalidParam(pos, format!("Expected parameter at position {}", pos))
})?;
self.validate(pos, &arg, format.clone())?;
}
Ok(())
}
fn validate(&self, pos: usize, arg: &str, format: CliFormat) -> Result<(), CliError> {
match format {
CliFormat::Any => return Ok(()),
CliFormat::Integer => {
arg.parse::<i64>().map_err(|_| {
CliError::InvalidParam(pos, format!("Expected integer, got '{}'", arg))
})?;
}
CliFormat::Decimal => {
arg.parse::<f64>().map_err(|_| {
CliError::InvalidParam(pos, format!("Expected decimal number, got '{}'", arg))
})?;
}
CliFormat::Boolean => {
if !["true", "false", "1", "0", "yes", "no"].contains(&arg.to_lowercase().as_str())
{
return Err(CliError::InvalidParam(
pos,
format!("Expected boolean (true/false/yes/no/1/0), got '{}'", arg),
));
}
}
CliFormat::Email => {
if !arg.contains('@') || !arg.contains('.') {
return Err(CliError::InvalidParam(
pos,
format!("Expected valid email, got '{}'", arg),
));
}
}
CliFormat::Url => {
Url::parse(arg).map_err(|_| {
CliError::InvalidParam(pos, format!("Expected valid URL, got '{}'", arg))
})?;
}
CliFormat::StringRange(range) => {
let len = arg.len();
if !range.contains(&len) {
return Err(CliError::InvalidParam(
pos,
format!(
"String length must be between {} and {}, got length {}",
range.start, range.end, len
),
));
}
}
CliFormat::IntegerRange(range) => {
let val = arg.parse::<i64>().map_err(|_| {
CliError::InvalidParam(pos, format!("Expected integer, got '{}'", arg))
})?;
if !range.contains(&val) {
return Err(CliError::InvalidParam(
pos,
format!(
"Integer must be between {} and {}, got {}",
range.start, range.end, val
),
));
}
}
CliFormat::DecimalRange(range) => {
let val = arg.parse::<f64>().map_err(|_| {
CliError::InvalidParam(pos, format!("Expected decimal, got '{}'", arg))
})?;
if val < range.start || val >= range.end {
return Err(CliError::InvalidParam(
pos,
format!(
"Decimal must be between {} and {}, got {}",
range.start, range.end, val
),
));
}
}
CliFormat::OneOf(options) => {
if !options.contains(&arg.to_string()) {
return Err(CliError::InvalidParam(
pos,
format!(
"Expected one of ({}), got '{}'",
options.join(" / ").to_string(),
arg
),
));
}
}
CliFormat::File => {
let metadata = fs::metadata(&arg)?;
if !metadata.is_file() {
return Err(CliError::InvalidParam(
pos,
format!("File does not exist, '{}'", arg),
));
}
}
CliFormat::Directory => {
let metadata = fs::metadata(&arg)?;
if !metadata.is_dir() {
return Err(CliError::InvalidParam(
pos,
format!("Directory does not exist, '{}'", arg),
));
}
}
};
Ok(())
}
}