getargs-derive 0.1.0

Clap-style #[derive] command-line argument parser for no_std and embedded Rust: zero allocation, compile-time help, subcommands. Built on getargs.
Documentation
#![allow(dead_code)]

use getargs_derive::{Error, GetArgs};

#[derive(Debug, GetArgs)]
struct Conflicts {
    #[arg(short, long, conflicts_with = "verbose")]
    quiet: bool,
    #[arg(short, long)]
    verbose: bool,
    #[arg(long, default_value_t = 1, conflicts_with = "quiet")]
    level: u8,
}

#[test]
fn conflicts_with_allows_either_argument() {
    let params = Conflicts::from_args(["-q"].into_iter()).unwrap();
    assert!(params.quiet);
    let params = Conflicts::from_args(["-v"].into_iter()).unwrap();
    assert!(params.verbose);
}

#[test]
fn conflicts_with_rejects_both_arguments() {
    let err = Conflicts::from_args(["-v", "--quiet"].into_iter()).unwrap_err();
    assert_eq!(
        err,
        Error::ArgumentConflict {
            name: "--quiet",
            other: "--verbose",
        }
    );
    assert_eq!(
        err.to_string(),
        "the argument '--quiet' cannot be used with '--verbose'"
    );
}

#[test]
fn conflicts_ignore_defaults() {
    let params = Conflicts::from_args(["--quiet"].into_iter()).unwrap();
    assert_eq!(params.level, 1);
    let err = Conflicts::from_args(["--quiet", "--level", "2"].into_iter()).unwrap_err();
    assert_eq!(
        err,
        Error::ArgumentConflict {
            name: "--level <LEVEL>",
            other: "--quiet",
        }
    );
}

#[derive(Debug, GetArgs)]
struct Requires<'a> {
    #[arg(short, long, requires = "output", requires = "level")]
    compress: bool,
    #[arg(short, long)]
    output: Option<&'a str>,
    #[arg(long, default_value_t = 6)]
    level: u8,
}

#[test]
fn requires_accepts_present_requirements() {
    let args = ["-c", "-o", "out.gz", "--level", "9"];
    let params = Requires::from_args(args.into_iter()).unwrap();
    assert!(params.compress);
    assert_eq!(params.output, Some("out.gz"));
    assert_eq!(params.level, 9);

    let params = Requires::from_args([].into_iter()).unwrap();
    assert!(!params.compress);
}

#[test]
fn requires_rejects_missing_requirement() {
    let err = Requires::from_args(["-c", "--level", "9"].into_iter()).unwrap_err();
    assert_eq!(
        err,
        Error::MissingRequiredArgument {
            name: "--output <OUTPUT>",
            required_by: "--compress",
        }
    );
    assert_eq!(
        err.to_string(),
        "the following required argument was not provided: --output <OUTPUT> (required by '--compress')"
    );
}

#[test]
fn requires_is_not_satisfied_by_a_default() {
    let err = Requires::from_args(["-c", "-o", "out.gz"].into_iter()).unwrap_err();
    assert_eq!(
        err,
        Error::MissingRequiredArgument {
            name: "--level <LEVEL>",
            required_by: "--compress",
        }
    );
}

#[derive(Debug, GetArgs)]
struct RequiredUnless<'a> {
    #[arg(short, long, required_unless_present = "stdin")]
    config: Option<&'a str>,
    #[arg(long)]
    stdin: bool,
    #[arg(required_unless_present = "stdin", required_unless_present = "config")]
    input: Option<&'a str>,
}

#[test]
fn required_unless_present_accepts_either() {
    let params = RequiredUnless::from_args(["-c", "app.toml", "in"].into_iter()).unwrap();
    assert_eq!(params.config, Some("app.toml"));
    assert_eq!(params.input, Some("in"));

    let params = RequiredUnless::from_args(["--stdin"].into_iter()).unwrap();
    assert_eq!(params.config, None);
    assert_eq!(params.input, None);
}

#[test]
fn required_unless_present_with_several_alternatives() {
    let params = RequiredUnless::from_args(["-c", "app.toml"].into_iter()).unwrap();
    assert_eq!(params.input, None);
}

#[test]
fn required_unless_present_rejects_none() {
    let err = RequiredUnless::from_args([].into_iter()).unwrap_err();
    assert_eq!(err, Error::MissingOneOf(&["--config <CONFIG>", "--stdin"]));
    assert_eq!(
        err.to_string(),
        "the following required arguments were not provided: <--config <CONFIG>|--stdin>"
    );
}

#[derive(Debug, GetArgs)]
#[command(group(id = "mode", required))]
struct ExclusiveGroup {
    #[arg(long, group = "mode")]
    fast: bool,
    #[arg(long, group = "mode")]
    slow: bool,
    #[arg(long, group = "mode")]
    speed: Option<u32>,
}

#[test]
fn exclusive_group_accepts_one_member() {
    let params = ExclusiveGroup::from_args(["--slow"].into_iter()).unwrap();
    assert!(params.slow);
    let params = ExclusiveGroup::from_args(["--speed", "3"].into_iter()).unwrap();
    assert_eq!(params.speed, Some(3));
}

#[test]
fn exclusive_group_rejects_two_members() {
    let err = ExclusiveGroup::from_args(["--speed=3", "--fast"].into_iter()).unwrap_err();
    assert_eq!(
        err,
        Error::ArgumentConflict {
            name: "--fast",
            other: "--speed <SPEED>",
        }
    );
}

#[test]
fn required_group_rejects_no_member() {
    let err = ExclusiveGroup::from_args([].into_iter()).unwrap_err();
    assert_eq!(
        err,
        Error::MissingOneOf(&["--fast", "--slow", "--speed <SPEED>"])
    );
    assert_eq!(
        err.to_string(),
        "the following required arguments were not provided: <--fast|--slow|--speed <SPEED>>"
    );
}

#[derive(Debug, GetArgs)]
#[command(group(id = "source", required, multiple))]
struct MultipleGroup<'a> {
    #[arg(long, group = "source")]
    file: Option<&'a str>,
    #[arg(long, group = "source")]
    url: Option<&'a str>,
}

#[test]
fn multiple_group_accepts_several_members() {
    let params = MultipleGroup::from_args(["--file", "a", "--url", "b"].into_iter()).unwrap();
    assert_eq!(params.file, Some("a"));
    assert_eq!(params.url, Some("b"));
}

#[test]
fn multiple_group_still_requires_one_member() {
    let err = MultipleGroup::from_args([].into_iter()).unwrap_err();
    assert_eq!(err, Error::MissingOneOf(&["--file <FILE>", "--url <URL>"]));
}

#[derive(Debug, GetArgs)]
struct ImplicitGroup {
    #[arg(short, group = "format")]
    json: bool,
    #[arg(short, group = "format")]
    yaml: bool,
}

#[test]
fn implicit_group_is_exclusive_and_optional() {
    let params = ImplicitGroup::from_args([].into_iter()).unwrap();
    assert!(!params.json && !params.yaml);
    let err = ImplicitGroup::from_args(["-jy"].into_iter()).unwrap_err();
    assert_eq!(
        err,
        Error::ArgumentConflict {
            name: "-j",
            other: "-y",
        }
    );
}

#[derive(Debug, GetArgs)]
struct Ordering {
    #[arg(long, conflicts_with = "b", requires = "c")]
    a: bool,
    #[arg(long)]
    b: bool,
    #[arg(long)]
    c: bool,
}

#[test]
fn conflicts_are_reported_before_requirements() {
    let err = Ordering::from_args(["--a", "--b"].into_iter()).unwrap_err();
    assert_eq!(
        err,
        Error::ArgumentConflict {
            name: "--a",
            other: "--b",
        }
    );
}