cchmod 0.1.3

chmod calculator.
Documentation
use cchmod::{
    traits::{AsNum, AsSym},
    Mode, Perm,
};
use clap::{crate_authors, crate_description, crate_name, crate_version, Parser};

#[derive(Parser)]
#[clap(name = crate_name!(), version = crate_version!(), author = crate_authors!(), about = crate_description!())]
pub struct Opts {
    input: String,

    #[clap(short, long, help = "Output the octal form")]
    num: bool,
    #[clap(short, long, help = "Output the symbolic form")]
    sym: bool,
}

fn main() {
    if let Err(message) = cli() {
        eprintln!("{}", message);
        std::process::exit(1);
    }
}

fn cli() -> Result<(), String> {
    let Opts { input, num, sym } = Opts::parse();

    let num = output_as_num(num, sym)?;
    let output = match try_parse(&input) {
        Some(Parsed::Mode(mode)) => convert(&mode, num),
        Some(Parsed::Perm(perm)) => convert(&perm, num),
        None => return Err(format!("{}: malformed permission or mode", input)),
    };

    println!("{}", output);

    Ok(())
}

fn output_as_num(num: bool, sym: bool) -> Result<bool, String> {
    if num && sym {
        return Err("--num and --sym are exclusive".to_string());
    } else if !(num || sym) {
        return Err("--num or --sym must be supplied".to_string());
    }

    Ok(num)
}

fn convert<T: AsNum + AsSym>(v: &T, as_num: bool) -> String {
    if as_num {
        v.as_num()
    } else {
        v.as_sym()
    }
}

#[derive(Debug, PartialEq)]
enum Parsed {
    Mode(Mode),
    Perm(Perm),
}

fn try_parse(input: &str) -> Option<Parsed> {
    if let Ok(mode) = Mode::from_num(input).or_else(|_| Mode::from_sym(input)) {
        Some(Parsed::Mode(mode))
    } else if let Ok(perm) = Perm::from_num(input).or_else(|_| Perm::from_sym_full(input)) {
        Some(Parsed::Perm(perm))
    } else {
        None
    }
}

#[cfg(test)]
mod test {

    #[test]
    fn test_output_as_num() {
        macro_rules! test {
            ($c:expr, $n:expr, $s:expr) => {
                assert_eq!($c, super::output_as_num($n, $s))
            };
        }

        test!(Ok(true), true, false);
        test!(Ok(false), false, true);

        test!(Err("--num and --sym are exclusive".to_string()), true, true);
        test!(
            Err("--num or --sym must be supplied".to_string()),
            false,
            false
        );
    }

    #[test]
    fn test_try_parse() {
        use super::Parsed::*;
        use cchmod::{Mode, Perm};

        macro_rules! test {
            ($c:expr, $input:expr) => {
                assert_eq!(Some($c), super::try_parse($input))
            };
        }

        macro_rules! test_fail {
            ($input:expr) => {
                assert_eq!(None, super::try_parse($input))
            };
        }

        test!(
            Mode(Mode {
                user: Perm {
                    read: true,
                    write: true,
                    execute: true
                },
                group: Perm {
                    read: true,
                    write: true,
                    execute: true
                },
                other: Perm {
                    read: true,
                    write: true,
                    execute: true
                }
            }),
            "rwxrwxrwx"
        );
        test!(
            Mode(Mode {
                user: Perm {
                    read: true,
                    write: true,
                    execute: true
                },
                group: Perm {
                    read: true,
                    write: true,
                    execute: true
                },
                other: Perm {
                    read: true,
                    write: true,
                    execute: true
                }
            }),
            "777"
        );
        test!(
            Perm(Perm {
                read: true,
                write: true,
                execute: true
            }),
            "rwx"
        );
        test!(
            Perm(Perm {
                read: true,
                write: true,
                execute: true
            }),
            "7"
        );

        test_fail!("");
        test_fail!("rx");
        test_fail!("rwxx");
        test_fail!("rwxrwx");
        test_fail!("8");
        test_fail!("77");
        test_fail!("585");
        test_fail!("4444");
    }
}