ropts 0.0.1

A simple command line argument parser for Rust
Documentation
use std::collections::HashMap;
use std::fmt::Debug;
pub mod utils;
pub mod value_option;
pub mod values_option;
use crate::error::Error;
pub use value_option::ValueOption;
pub use values_option::ValuesOption;

pub struct OptionBaseAttributes<'a, T>
where
    T: Debug + Clone,
{
    description: String,
    env_key: Option<String>,
    long_arg: Option<String>,
    short_arg: Option<char>,
    required: bool,
    default: Option<T>,
    value: &'a mut Option<T>,
    additional_eval: Option<Box<dyn Fn(&T) -> Result<(), Error> + 'a>>,
    type_id: std::any::TypeId,
}

pub trait OptionBase {
    fn parse_env(&mut self, envs: &HashMap<String, String>);
    fn parse_args(&mut self, args: &[String]);
    fn eval(&mut self) -> Result<(), Error>;
    fn help(&self) -> String;
}

impl<'a, T> OptionBaseAttributes<'a, T>
where
    T: Debug + Clone,
{
    fn identifier(&self) -> String {
        let mut identifier = "{".to_string();

        if let Some(env_key) = &self.env_key {
            identifier.push_str(&format!("{}, ", env_key));
        }
        if let Some(short_arg) = self.short_arg {
            identifier.push_str(&format!("-{}, ", short_arg));
        }
        if let Some(long_arg) = &self.long_arg {
            identifier.push_str(&format!("--{}, ", long_arg));
        }

        identifier = identifier.trim_end_matches(", ").to_string();
        identifier.push_str("}");
        identifier
    }
}

impl<'a, T> OptionBase for OptionBaseAttributes<'a, T>
where
    T: Debug + Clone,
{
    fn parse_env(&mut self, _: &HashMap<String, String>) {
        panic!("Not implemented, needs to be handled by the option type");
    }

    fn parse_args(&mut self, _: &[String]) {
        panic!("Not implemented, needs to be handled by the option type");
    }

    fn eval(&mut self) -> Result<(), Error> {
        if self.env_key.is_none() && self.long_arg.is_none() && self.short_arg.is_none() {
            return Err(Error::Parsing("No identifier set".into()));
        }

        if self.required && self.value.is_none() {
            return Err(Error::Validation(format!(
                "{} is required",
                self.identifier()
            )));
        }
        if self.value.is_none() {
            *self.value = self.default.clone();
        }
        if self.additional_eval.is_some() {
            let validation_result =
                self.additional_eval.as_ref().unwrap()(&self.value.as_ref().unwrap());
            if validation_result.is_err() {
                return Err(Error::Validation(format!(
                    "{} failed validation: {}",
                    self.identifier(),
                    validation_result.unwrap_err()
                )));
            }
        }

        Ok(())
    }

    fn help(&self) -> String {
        let mut help = String::new();

        if let Some(env_key) = &self.env_key {
            help.push_str(&format!("ENV:{} ", env_key));
        }
        if self.long_arg.is_some() || self.short_arg.is_some() {
            help.push_str(&format!("ARGS:"));
            if let Some(short_arg) = self.short_arg {
                help.push_str(&format!("-{},", short_arg));
            }
            if let Some(long_arg) = &self.long_arg {
                help.push_str(&format!("--{},", long_arg));
            }
            help.pop();
        }

        if self.required {
            help.push_str("  Required");
        } else if let Some(default) = &self.default {
            help.push_str(&format!("  Default: {:?}\n", default));
        }

        help.push_str(&format!(" - {}", self.description));

        help
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn identifier_env() {
        let mut value = None::<String>;
        let opt = OptionBaseAttributes::<String> {
            description: "".to_string(),
            env_key: Some("ENV_KEY".to_string()),
            long_arg: None,
            short_arg: None,
            required: false,
            default: None,
            value: &mut value,
            additional_eval: None,
            type_id: std::any::TypeId::of::<String>(),
        };
        assert_eq!(opt.identifier(), "{ENV_KEY}");
    }

    #[test]
    fn identifier_short_arg() {
        let mut value = None::<String>;
        let opt = OptionBaseAttributes::<String> {
            description: "".to_string(),
            env_key: None,
            long_arg: None,
            short_arg: Some('i'),
            required: false,
            default: None,
            value: &mut value,
            additional_eval: None,
            type_id: std::any::TypeId::of::<String>(),
        };
        assert_eq!(opt.identifier(), "{-i}");
    }

    #[test]
    fn identifier_long_arg() {
        let mut value = None::<String>;
        let opt = OptionBaseAttributes::<String> {
            description: "".to_string(),
            env_key: None,
            long_arg: Some("long_arg".to_string()),
            short_arg: None,
            required: false,
            default: None,
            value: &mut value,
            additional_eval: None,
            type_id: std::any::TypeId::of::<String>(),
        };
        assert_eq!(opt.identifier(), "{--long_arg}");
    }

    #[test]
    fn identifier_all() {
        let mut value = None::<String>;
        let opt = OptionBaseAttributes::<String> {
            description: "".to_string(),
            env_key: Some("ENV_KEY".to_string()),
            long_arg: Some("long_arg".to_string()),
            short_arg: Some('i'),
            required: false,
            default: None,
            value: &mut value,
            additional_eval: None,
            type_id: std::any::TypeId::of::<String>(),
        };
        assert_eq!(opt.identifier(), "{ENV_KEY, -i, --long_arg}");
    }

    #[test]
    fn help_env() {
        let mut value = None::<String>;
        let opt = OptionBaseAttributes::<String> {
            description: "my description".to_string(),
            env_key: Some("ENV_KEY".to_string()),
            long_arg: None,
            short_arg: None,
            required: false,
            default: None,
            value: &mut value,
            additional_eval: None,
            type_id: std::any::TypeId::of::<String>(),
        };
        assert_eq!(opt.help(), "ENV:ENV_KEY  - my description");
    }

    #[test]
    fn help_short_arg() {
        let mut value = None::<String>;
        let opt = OptionBaseAttributes::<String> {
            description: "my description".to_string(),
            env_key: None,
            long_arg: None,
            short_arg: Some('i'),
            required: false,
            default: None,
            value: &mut value,
            additional_eval: None,
            type_id: std::any::TypeId::of::<String>(),
        };
        assert_eq!(opt.help(), "ARGS:-i - my description");
    }

    #[test]
    fn help_long_arg() {
        let mut value = None::<String>;
        let opt = OptionBaseAttributes::<String> {
            description: "my description".to_string(),
            env_key: None,
            long_arg: Some("long_arg".to_string()),
            short_arg: None,
            required: false,
            default: None,
            value: &mut value,
            additional_eval: None,
            type_id: std::any::TypeId::of::<String>(),
        };
        assert_eq!(opt.help(), "ARGS:--long_arg - my description");
    }

    #[test]
    fn help_all() {
        let mut value = None::<String>;
        let opt = OptionBaseAttributes::<String> {
            description: "my description".to_string(),
            env_key: Some("ENV_KEY".to_string()),
            long_arg: Some("long_arg".to_string()),
            short_arg: Some('i'),
            required: false,
            default: None,
            value: &mut value,
            additional_eval: None,
            type_id: std::any::TypeId::of::<String>(),
        };
        assert_eq!(
            opt.help(),
            "ENV:ENV_KEY ARGS:-i,--long_arg - my description"
        );
    }

    #[test]
    fn eval_no_identifier() {
        let mut value = None::<String>;
        let mut opt = OptionBaseAttributes::<String> {
            description: "my description".to_string(),
            env_key: None,
            long_arg: None,
            short_arg: None,
            required: false,
            default: None,
            value: &mut value,
            additional_eval: None,
            type_id: std::any::TypeId::of::<String>(),
        };
        assert!(opt.eval().is_err());
    }

    #[test]
    fn eval_required() {
        let mut value = None::<String>;
        let mut opt = OptionBaseAttributes::<String> {
            description: "my description".to_string(),
            env_key: Some("ENV_KEY".to_string()),
            long_arg: None,
            short_arg: None,
            required: true,
            default: None,
            value: &mut value,
            additional_eval: None,
            type_id: std::any::TypeId::of::<String>(),
        };
        assert!(opt.eval().is_err());
    }

    #[test]
    fn eval_default() {
        let mut value = None::<String>;
        {
            let mut opt = OptionBaseAttributes::<String> {
                description: "my description".to_string(),
                env_key: Some("ENV_KEY".to_string()),
                long_arg: None,
                short_arg: None,
                required: false,
                default: Some("default".to_string()),
                value: &mut value,
                additional_eval: None,
                type_id: std::any::TypeId::of::<String>(),
            };
            assert!(opt.eval().is_ok());
        }
        assert_eq!(value, Some("default".to_string()));
    }

    #[test]
    fn eval_additional_eval() {
        let mut value = Some("value".to_string());
        let mut opt = OptionBaseAttributes::<String> {
            description: "my description".to_string(),
            env_key: Some("ENV_KEY".to_string()),
            long_arg: None,
            short_arg: None,
            required: false,
            default: None,
            value: &mut value,
            additional_eval: Some(Box::new(|_| Ok(()))),
            type_id: std::any::TypeId::of::<String>(),
        };
        assert!(opt.eval().is_ok());
    }

    #[test]
    fn eval_additional_eval_fail() {
        let mut value = Some("value".to_string());
        let mut opt = OptionBaseAttributes::<String> {
            description: "my description".to_string(),
            env_key: Some("ENV_KEY".to_string()),
            long_arg: None,
            short_arg: None,
            required: false,
            default: None,
            value: &mut value,
            additional_eval: Some(Box::new(|_| Err(Error::Validation("fail".into())))),
            type_id: std::any::TypeId::of::<String>(),
        };
        assert!(opt.eval().is_err());
    }
}