appcore-args 1.0.1-rc.8

Bounded cross-platform argument parsing, help and completion for AppCore executables
Documentation
use crate::{
    ArgLimits, ArgumentSpec, CliErrorKind, CliParser, CliSpec, CommandSpec, OptionSpec, RawArgs,
    ValueType,
};

fn spec() -> CliSpec {
    CliSpec::new("demo")
        .option(OptionSpec::flag("verbose").short('v'))
        .command_required(true)
        .command(
            CommandSpec::new("publish")
                .option(OptionSpec::flag("force").short('f'))
                .command(CommandSpec::new("status").argument(ArgumentSpec::new("crate"))),
        )
}

#[test]
fn parses_nested_inherited_options_and_positionals() {
    let args = RawArgs::parse(["publish", "-v", "status", "-f", "crate"]).unwrap();
    let parsed = CliParser::new(&spec()).parse(&args).unwrap();
    assert_eq!(parsed.command_path(), &["publish", "status"]);
    assert!(parsed.has_flag("verbose"));
    assert!(parsed.has_flag("force"));
    assert_eq!(parsed.positionals(), &["crate"]);
}

#[test]
fn parses_grouped_flags_and_attached_short_value() {
    let spec = CliSpec::new("demo")
        .option(OptionSpec::flag("all").short('a'))
        .option(OptionSpec::flag("verbose").short('v'))
        .option(OptionSpec::value("output").short('o'));
    let parsed = CliParser::new(&spec)
        .parse(&RawArgs::parse(["-avoresult.txt"]).unwrap())
        .unwrap();
    assert!(parsed.has_flag("all"));
    assert!(parsed.has_flag("verbose"));
    assert_eq!(parsed.option_value("output"), Some("result.txt"));
}

#[test]
fn validates_values_and_required_arguments() {
    let spec = CliSpec::new("demo")
        .option(OptionSpec::value("count").value_type(ValueType::U64))
        .argument(
            ArgumentSpec::new("mode")
                .required(true)
                .possible_value("fast"),
        );
    let invalid = CliParser::new(&spec)
        .parse(&RawArgs::parse(["--count", "x", "fast"]).unwrap())
        .unwrap_err();
    assert_eq!(invalid.kind(), CliErrorKind::InvalidValue);
    let missing = CliParser::new(&spec)
        .parse(&RawArgs::parse(Vec::<String>::new()).unwrap())
        .unwrap_err();
    assert_eq!(missing.kind(), CliErrorKind::MissingValue);
}

#[test]
fn enforces_option_relationships() {
    let spec = CliSpec::new("demo")
        .option(OptionSpec::flag("json").conflicts_with("plain"))
        .option(OptionSpec::flag("plain"));
    let error = CliParser::new(&spec)
        .parse(&RawArgs::parse(["--json", "--plain"]).unwrap())
        .unwrap_err();
    assert_eq!(error.kind(), CliErrorKind::OptionConflict);
}

#[test]
fn bounds_raw_arguments_but_accepts_empty_values() {
    let limits = ArgLimits {
        max_words: 1,
        max_word_bytes: 4,
        max_total_bytes: 4,
    };
    assert!(RawArgs::parse_with_limits([""], limits).is_ok());
    assert_eq!(
        RawArgs::parse_with_limits(["hello"], limits)
            .unwrap_err()
            .kind(),
        CliErrorKind::InvalidInput
    );
}

#[test]
fn terminal_option_bypasses_required_inputs() {
    let spec = CliSpec::new("demo")
        .command_required(true)
        .option(OptionSpec::flag("help").terminal(true))
        .command(CommandSpec::new("run").argument(ArgumentSpec::new("file").required(true)));
    let parsed = CliParser::new(&spec)
        .parse(&RawArgs::parse(["--help"]).unwrap())
        .unwrap();
    assert!(parsed.has_flag("help"));
}

#[test]
fn optional_values_only_consume_unambiguous_attached_input() {
    let spec = CliSpec::new("demo")
        .option(OptionSpec::value("color").short('c').optional_value())
        .argument(ArgumentSpec::new("file"));

    let detached = CliParser::new(&spec)
        .parse(&RawArgs::parse(["--color", "input.txt"]).unwrap())
        .unwrap();
    assert_eq!(detached.option_value("color"), None);
    assert_eq!(detached.positionals(), &["input.txt"]);

    let long = CliParser::new(&spec)
        .parse(&RawArgs::parse(["--color=blue"]).unwrap())
        .unwrap();
    let short = CliParser::new(&spec)
        .parse(&RawArgs::parse(["-cblue"]).unwrap())
        .unwrap();
    assert_eq!(long.option_value("color"), Some("blue"));
    assert_eq!(short.option_value("color"), Some("blue"));
}

#[test]
fn preserves_passthrough_without_parsing_it_as_options() {
    let parsed = CliParser::new(&CliSpec::new("demo"))
        .parse(&RawArgs::parse(["--", "--unknown", "value"]).unwrap())
        .unwrap();

    assert_eq!(parsed.passthrough(), &["--unknown", "value"]);
}

#[test]
fn rejects_nul_bytes_at_the_raw_input_boundary() {
    let error = RawArgs::parse(["bad\0value"]).unwrap_err();
    assert_eq!(error.kind(), CliErrorKind::InvalidInput);
}