#![cfg(feature = "derive")]
use pound::{
Error,
Parse,
ValueEnum,
};
fn argv<'a>(a: &[&'a str]) -> Vec<&'a str> {
a.to_vec()
}
#[derive(Parse)]
#[pound(name = "sandbox", version = "0.1.0")]
struct Sandbox {
#[pound(short, long)]
sockets: bool,
#[pound(short, long)]
env: Vec<String>,
#[pound(trailing)]
exec: Vec<String>,
}
#[test]
fn sandbox_flat() {
let y = Sandbox::parse_from(argv(&["-s", "--env", "A=1", "-e", "B=2", "--", "ls", "-la"]));
assert!(y.sockets);
assert_eq!(y.env, ["A=1", "B=2"]);
assert_eq!(y.exec, ["ls", "-la"]);
}
#[derive(Parse)]
#[pound(name = "build")]
struct Build {
target: Option<String>,
#[pound(short, long, count)]
verbose: u8,
}
#[test]
fn counts_and_optionals() {
let b = Build::parse_from(argv(&["-vvv"]));
assert_eq!(b.verbose, 3);
assert_eq!(b.target, None);
let b = Build::parse_from(argv(&["release", "--verbose"]));
assert_eq!(b.verbose, 1);
assert_eq!(b.target.as_deref(), Some("release"));
}
#[derive(Parse, Debug, PartialEq, Eq)]
#[pound(name = "pkg", version = "1.0.0")]
enum Pkg {
Init {
#[pound(short, long)]
force: bool,
},
Add {
name: String,
url: String,
#[pound(short, long)]
force: bool,
},
}
#[test]
fn pkg_subcommands() {
assert_eq!(Pkg::parse_from(argv(&["init", "--force"])), Pkg::Init { force: true });
assert_eq!(
Pkg::parse_from(argv(&["add", "serde", "https://x", "-f"])),
Pkg::Add { name: "serde".into(), url: "https://x".into(), force: true }
);
}
#[test]
fn pkg_help_screen() {
let Err(Error::Help(text)) = Pkg::try_parse_from(argv(&["--help"])) else {
panic!("expected help");
};
assert!(text.contains("Usage: pkg"));
#[cfg(feature = "help")]
{
assert!(text.contains("Commands:"));
assert!(text.contains("initialise a project"));
}
}
#[derive(ValueEnum, Debug, PartialEq, Eq)]
enum Mode {
Fast,
DoubleSpeed,
}
#[derive(Parse)]
#[pound(name = "run")]
struct Run {
#[pound(long)]
mode: Mode,
}
#[test]
fn derived_value_enum() {
assert_eq!(Run::parse_from(argv(&["--mode", "fast"])).mode, Mode::Fast);
assert_eq!(Run::parse_from(argv(&["--mode", "double-speed"])).mode, Mode::DoubleSpeed);
match Run::try_parse_from(argv(&["--mode", "warp"])) {
Err(Error::Value { value, .. }) => assert_eq!(value, "warp"),
_ => panic!("expected a value error"),
}
}
#[derive(Parse)]
#[pound(name = "pick", required_group = "speed")]
#[allow(dead_code, reason = "only the parse outcome is asserted")]
struct Pick {
#[pound(long, group = "speed")]
fast: bool,
#[pound(long, group = "speed")]
slow: bool,
}
#[test]
fn required_group() {
assert!(Pick::try_parse_from(argv(&["--fast"])).is_ok());
assert!(matches!(Pick::try_parse_from(argv(&[])), Err(Error::MissingGroup { .. })));
assert!(matches!(
Pick::try_parse_from(argv(&["--fast", "--slow"])),
Err(Error::Conflict { .. })
));
}
#[derive(Parse)]
#[pound(name = "log")]
#[allow(dead_code, reason = "only the parse outcome is asserted")]
struct Log {
#[pound(long)]
quiet: bool,
#[pound(long, conflicts_with = "quiet")]
verbose: bool,
}
#[test]
fn conflicts_with() {
assert!(Log::try_parse_from(argv(&["--quiet"])).is_ok());
assert!(Log::try_parse_from(argv(&["--verbose"])).is_ok());
assert!(matches!(
Log::try_parse_from(argv(&["--quiet", "--verbose"])),
Err(Error::Conflict { .. })
));
}
#[derive(Parse, Debug, PartialEq, Eq)]
#[pound(name = "limit")]
struct Limit {
#[pound(long, min = "5", max = "20")]
count: u64,
#[pound(long, max_len = "9")]
name: String,
#[pound(long, validate = "even")]
shard: u64,
}
#[derive(Parse, Debug, PartialEq, Eq)]
#[pound(name = "fallback-limit")]
struct FallbackLimit {
#[pound(long, default = "8", min = "5", max = "20", validate = "even")]
count: u64,
#[pound(long, max_len = "4")]
label: Option<String>,
}
#[allow(
clippy::missing_const_for_fn,
clippy::trivially_copy_pass_by_ref,
reason = "validator hooks receive the parsed field by reference"
)]
fn even(value: &u64) -> Result<(), &'static str> {
if value.is_multiple_of(2) {
Ok(())
} else {
Err("must be even")
}
}
#[test]
fn validated_values() {
let parsed = Limit::parse_from(argv(&[
"--count", "12", "--name", "short", "--shard", "2",
]));
assert_eq!(parsed.count, 12);
assert_eq!(parsed.name, "short");
assert_eq!(parsed.shard, 2);
match Limit::try_parse_from(argv(&[
"--count", "4", "--name", "short", "--shard", "2",
])) {
Err(Error::Value { value, msg, .. }) => {
assert_eq!(value, "4");
assert_eq!(msg, "must be at least 5");
},
other => panic!("expected lower bound value error, got {other:?}"),
}
match Limit::try_parse_from(argv(&[
"--count", "21", "--name", "short", "--shard", "2",
])) {
Err(Error::Value { value, msg, .. }) => {
assert_eq!(value, "21");
assert_eq!(msg, "must be at most 20");
},
other => panic!("expected upper bound value error, got {other:?}"),
}
match Limit::try_parse_from(argv(&[
"--count",
"12",
"--name",
"very-long-name",
"--shard",
"2",
])) {
Err(Error::Value { value, msg, .. }) => {
assert_eq!(value, "very-long-name");
assert_eq!(msg, "must be at most 9 chars");
},
other => panic!("expected length value error, got {other:?}"),
}
match Limit::try_parse_from(argv(&[
"--count", "12", "--name", "short", "--shard", "3",
])) {
Err(Error::Value { value, msg, .. }) => {
assert_eq!(value, "3");
assert_eq!(msg, "must be even");
},
other => panic!("expected custom validation error, got {other:?}"),
}
}
#[test]
fn validation_runs_for_defaults_and_options() {
let parsed = FallbackLimit::parse_from(argv(&[]));
assert_eq!(parsed.count, 8);
assert_eq!(parsed.label, None);
let parsed = FallbackLimit::parse_from(argv(&["--label", "mini"]));
assert_eq!(parsed.label.as_deref(), Some("mini"));
match FallbackLimit::try_parse_from(argv(&["--count", "3"])) {
Err(Error::Value { value, msg, .. }) => {
assert_eq!(value, "3");
assert_eq!(msg, "must be at least 5");
},
other => panic!("expected defaulted field validation error, got {other:?}"),
}
match FallbackLimit::try_parse_from(argv(&["--label", "large"])) {
Err(Error::Value { value, msg, .. }) => {
assert_eq!(value, "large");
assert_eq!(msg, "must be at most 4 chars");
},
other => panic!("expected optional field length error, got {other:?}"),
}
}
#[derive(Parse)]
#[pound(name = "batch")]
struct Batch {
#[pound(long, min = "5", max = "20")]
counts: Vec<u64>,
}
#[test]
fn validated_repeatable_values() {
assert_eq!(
Batch::parse_from(argv(&["--counts", "5", "--counts", "20"])).counts,
[5, 20]
);
assert!(matches!(
Batch::try_parse_from(argv(&["--counts", "5", "--counts", "40"])),
Err(Error::Value { value, .. }) if value == "40"
));
}
#[derive(Debug, PartialEq, Eq)]
struct HexByte(u8);
fn hex_byte(value: &str) -> Result<HexByte, &'static str> {
let value = value.strip_prefix("0x").ok_or("expected 0xNN")?;
u8::from_str_radix(value, 16)
.map(HexByte)
.map_err(|_| "expected two hex digits")
}
const fn non_zero_hex(value: &HexByte) -> Result<(), &'static str> {
if value.0 == 0 {
Err("must not be zero")
} else {
Ok(())
}
}
#[derive(Parse, Debug, PartialEq, Eq)]
#[pound(name = "hex")]
struct Hex {
#[pound(long, parse = "hex_byte", validate = "non_zero_hex")]
byte: HexByte,
#[pound(long, parse = "hex_byte")]
many: Vec<HexByte>,
#[pound(long, parse = "hex_byte")]
maybe: Option<HexByte>,
#[pound(long, default = "0x10", parse = "hex_byte")]
defaulted: HexByte,
}
#[test]
fn custom_parsers() {
let parsed = Hex::parse_from(argv(&["--byte", "0x2a", "--many", "0x01", "--many", "0xff"]));
assert_eq!(parsed.byte, HexByte(42));
assert_eq!(parsed.many, [HexByte(1), HexByte(255)]);
assert_eq!(parsed.maybe, None);
assert_eq!(parsed.defaulted, HexByte(16));
let parsed = Hex::parse_from(argv(&[
"--byte",
"0x2a",
"--maybe",
"0x03",
"--defaulted",
"0x04",
]));
assert_eq!(parsed.maybe, Some(HexByte(3)));
assert_eq!(parsed.defaulted, HexByte(4));
match Hex::try_parse_from(argv(&["--byte", "2a"])) {
Err(Error::Value { value, msg, .. }) => {
assert_eq!(value, "2a");
assert_eq!(msg, "expected 0xNN");
},
other => panic!("expected custom parser error, got {other:?}"),
}
match Hex::try_parse_from(argv(&["--byte", "0x00"])) {
Err(Error::Value { value, msg, .. }) => {
assert_eq!(value, "0x00");
assert_eq!(msg, "must not be zero");
},
other => panic!("expected custom parser validation error, got {other:?}"),
}
}