scalp 0.4.3

A declarative parsing library for pretty and highly customizable command-line interfaces.
Documentation
use checkito::*;
use scalp::{Case, Error, Parser};
use std::{error, result, str::FromStr};

type Result = result::Result<(), Box<dyn error::Error>>;
const COUNT: usize = 1000;

#[test]
fn empty_parser_builds() -> Result {
    Parser::builder().build()?;
    Ok(())
}

#[test]
fn empty_parser_with_name_builds() -> Result {
    String::generator().check(COUNT, |name| {
        Parser::builder().name(name.clone()).build().is_ok()
    })?;
    Ok(())
}

#[test]
fn empty_parser_with_help_builds() -> Result {
    String::generator().check(COUNT, |name| {
        Parser::builder().help(name.clone()).build().is_ok()
    })?;
    Ok(())
}

#[test]
fn missing_option_value_with_short() -> Result {
    (regex!("[~±@£¢¤¬¦²³¼½¾`^¯_]+"), regex!("[a-zA-Z]")).check(COUNT, |(short, name)| {
        let parser = Parser::builder()
            .case(Case::Same)
            .prefix(short.clone(), "--")
            .option::<usize, _>(|option| option.name(name.clone()))
            .build()
            .unwrap();
        let argument = format!("{short}{name}");
        let error = parser.parse_with([argument.clone()], [("", "")]).unwrap_err();
        prove!(matches!(error, Error::MissingOptionValue(type_name, path) if type_name == Some("natural number".into()) && path == vec![argument.into()]))
    })?;
    Ok(())
}

#[test]
fn missing_option_value_with_long() -> Result {
    (regex!("[~±@£¢¤¬¦²³¼½¾`^¯_]+"), regex!("[a-zA-Z]{2,}")).check(COUNT, |(long, name)| {
        let parser = Parser::builder()
            .case(Case::Same)
            .prefix("-", long.clone())
            .option::<isize, _>(|option| option.name(name.clone()))
            .build()
            .unwrap();
        let argument = format!("{long}{name}");
        let error = parser.parse_with([argument.clone()], [("", "")]).unwrap_err();
        prove!(matches!(error, Error::MissingOptionValue(type_name, path) if type_name == Some("integer number".into()) && path == vec![argument.into()]))
    })?;
    Ok(())
}

#[test]
fn fails_to_parse_invalid_value() -> Result {
    (regex!("[a-zA-Z]{2,}"), ..-1).check(COUNT, |(name, value)| {
        let parser = Parser::builder()
            .case(Case::Same)
            .option::<usize, _>(|option| option.name(name.clone()))
            .build()
            .unwrap();
        let arguments = (format!("--{name}"), format!("{value}"));
        let error = parser
            .parse_with([arguments.0.clone(), arguments.1.clone()], [("", "")])
            .unwrap_err();
        prove!(matches!(error, Error::FailedToParseOptionValue(value, type_name, path) if value == arguments.1 && type_name == Some("natural number".into()) && path == vec!(arguments.0.into())))
    })?;
    Ok(())
}

#[test]
fn verb_with_no_option_allows_for_root_options_before_and_after() -> Result {
    (
        regex!("[a-z]{2,}")
            .array::<3>()
            .filter(|[a, b, c]| a != b && a != c && b != c),
        u8::generator(),
        u8::generator(),
    )
        .check(COUNT, |(values, v, u)| {
            let Some([a, b, c]) = values else {
                return Ok(true);
            };

            let (v, u) = (*v, *u);
            let parser = Parser::builder()
                .case(Case::Same)
                .option(|option| option.name(a.clone()).default(v))
                .option(|option| option.name(b.clone()).default(u))
                .verb(|verb| verb.name(c.clone()))
                .build()
                .unwrap();
            let result = parser.parse_with(
                [
                    format!("--{a}"),
                    format!("{v}"),
                    c.clone(),
                    format!("--{b}"),
                    format!("{u}"),
                ],
                [("", "")],
            );
            prove!(result == Ok((v, u, Some(()))))
        })?;
    Ok(())
}

#[test]
fn boolean_option_swizzling() -> Result {
    let parser = Parser::builder()
        .option(|option| option.name("a").swizzle().default(false))
        .option(|option| option.name("b").swizzle().default(false))
        .option(|option| option.name("c").swizzle().default(false))
        .build()?;
    assert_eq!(parser.parse_with(["-a"], [("", "")])?, (true, false, false));
    assert_eq!(parser.parse_with(["-ab"], [("", "")])?, (true, true, false));
    assert_eq!(parser.parse_with(["-abc"], [("", "")])?, (true, true, true));
    assert_eq!(parser.parse_with(["-ca"], [("", "")])?, (true, false, true));
    assert_eq!(parser.parse_with(["-bca"], [("", "")])?, (true, true, true));
    Ok(())
}

#[test]
fn invalid_swizzling() -> Result {
    let parser = Parser::builder()
        .option(|option| option.name("a").swizzle().default(false))
        .option(|option| option.name("b").default(false))
        .build()?;
    assert_eq!(parser.parse_with(["-a"], [("", "")]), Ok((true, false)));
    assert_eq!(parser.parse_with(["-b"], [("", "")]), Ok((false, true)));
    assert_eq!(
        parser.parse_with(["-ab"], [("", "")]),
        Err(Error::InvalidSwizzleOption('b'))
    );
    assert_eq!(
        parser.parse_with(["-ba"], [("", "")]),
        Err(Error::InvalidSwizzleOption('b'))
    );
    Ok(())
}

#[test]
fn parses_enum_value() -> Result {
    #[allow(non_camel_case_types)]
    #[derive(Debug, Clone, PartialEq)]
    enum Casing {
        Same,
        camelCase,
        PascalCase,
        snake_case,
    }

    impl FromStr for Casing {
        type Err = &'static str;

        fn from_str(s: &str) -> result::Result<Self, Self::Err> {
            match s {
                "same" => Ok(Casing::Same),
                "c" | "camel-case" => Ok(Casing::camelCase),
                "p" | "pascal-case" => Ok(Casing::PascalCase),
                "s" | "snake-case" => Ok(Casing::snake_case),
                _ => Err("Failed to parse."),
            }
        }
    }

    let parser = Parser::builder()
        .option::<Casing, _>(|option| {
            option
                .name("c")
                .valid("c(amel-case)?")
                .valid("p(ascal-case)?")
                .valid("s(nake-case)?")
                .default(Casing::Same)
        })
        .map(|(case,)| case)
        .build()?;
    assert_eq!(
        parser.parse_with(["-c", "camel-case"], [("", "")]),
        Ok(Casing::camelCase)
    );
    assert_eq!(
        parser.parse_with(["-c", "c"], [("", "")]),
        Ok(Casing::camelCase)
    );
    assert_eq!(
        parser.parse_with(["-c", "pascal-case"], [("", "")]),
        Ok(Casing::PascalCase)
    );
    assert_eq!(
        parser.parse_with(["-c", "p"], [("", "")]),
        Ok(Casing::PascalCase)
    );
    assert_eq!(
        parser.parse_with(["-c", "snake-case"], [("", "")]),
        Ok(Casing::snake_case)
    );
    assert_eq!(
        parser.parse_with(["-c", "s"], [("", "")]),
        Ok(Casing::snake_case)
    );
    assert_eq!(
        parser.parse_with(["-c", "same"], [("", "")]),
        Err(Error::InvalidOptionValue(
            "same".into(),
            ["c(amel-case)?", "p(ascal-case)?", "s(nake-case)?"]
                .map(ToString::to_string)
                .to_vec(),
            vec!["-c".into()]
        ))
    );
    Ok(())
}