#![allow(dead_code)]
use runi_cli::{
CommandSchema, Error, Launcher, OptionParser, ParseResult, Result, Runnable, SubCommandOf,
};
use runi_cli::Command as CommandDerive;
fn args(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[derive(CommandDerive, Debug)]
#[command(name = "greet")]
struct Greeter {
#[option("-l,--loud")]
loud: bool,
#[argument]
target: String,
}
impl Runnable for Greeter {
fn run(&self) -> Result<()> {
Ok(())
}
}
#[test]
fn struct_derive_basic() {
let schema = <Greeter as runi_cli::Command>::schema();
assert_eq!(schema.name, "greet");
assert_eq!(schema.description, "Say hello.");
let r = OptionParser::parse(&schema, &args(&["--loud", "world"])).unwrap();
let g = <Greeter as runi_cli::Command>::from_parsed(&r).unwrap();
assert!(g.loud);
assert_eq!(g.target, "world");
}
#[derive(CommandDerive)]
#[command(name = "build", description = "Build the project")]
struct BuildCmd {
#[option("-j,--jobs")]
jobs: Option<u32>,
#[option("-F,--feature")]
features: Vec<String>,
#[option("--target")]
target: String,
}
impl Runnable for BuildCmd {
fn run(&self) -> Result<()> {
Ok(())
}
}
#[test]
fn struct_derive_optional_and_repeatable() {
let schema = <BuildCmd as runi_cli::Command>::schema();
let r = OptionParser::parse(
&schema,
&args(&[
"--jobs",
"4",
"-F",
"alpha",
"--feature",
"beta",
"--target",
"x86_64-unknown-linux-gnu",
]),
)
.unwrap();
let b = <BuildCmd as runi_cli::Command>::from_parsed(&r).unwrap();
assert_eq!(b.jobs, Some(4));
assert_eq!(b.features, vec!["alpha".to_string(), "beta".to_string()]);
assert_eq!(b.target, "x86_64-unknown-linux-gnu");
}
#[test]
fn struct_derive_optional_absent_is_none() {
let schema = <BuildCmd as runi_cli::Command>::schema();
let r = OptionParser::parse(&schema, &args(&["--target", "aarch64-apple-darwin"])).unwrap();
let b = <BuildCmd as runi_cli::Command>::from_parsed(&r).unwrap();
assert!(b.jobs.is_none());
assert!(b.features.is_empty());
}
#[derive(CommandDerive)]
#[command(name = "count")]
struct CountCmd {
#[argument]
offset: i32,
#[argument]
out: Option<std::path::PathBuf>,
}
#[test]
fn struct_derive_typed_required_and_optional_positional() {
let schema = <CountCmd as runi_cli::Command>::schema();
let r = OptionParser::parse(&schema, &args(&["-5", "/tmp/x"])).unwrap();
let c = <CountCmd as runi_cli::Command>::from_parsed(&r).unwrap();
assert_eq!(c.offset, -5);
assert_eq!(c.out.as_deref(), Some(std::path::Path::new("/tmp/x")));
let r = OptionParser::parse(&schema, &args(&["42"])).unwrap();
let c = <CountCmd as runi_cli::Command>::from_parsed(&r).unwrap();
assert_eq!(c.offset, 42);
assert!(c.out.is_none());
}
#[derive(CommandDerive)]
#[command(name = "inline")]
struct InlineDesc {
#[option("-v,--verbose", description = "inline option description")]
verbose: bool,
#[argument(description = "inline argument description")]
path: String,
}
#[test]
fn inline_descriptions_parse() {
let schema = <InlineDesc as runi_cli::Command>::schema();
let opt = schema
.options
.iter()
.find(|o| o.matches_long("verbose"))
.unwrap();
assert_eq!(opt.description, "inline option description");
assert_eq!(
schema.arguments[0].description,
"inline argument description"
);
}
#[derive(CommandDerive)]
#[command(name = "described", description = "explicit description wins")]
struct Described {
#[option("-v,--verbose")]
verbose: bool,
}
#[test]
fn explicit_description_beats_doc_comment() {
let schema = <Described as runi_cli::Command>::schema();
assert_eq!(schema.description, "explicit description wins");
let opt = schema
.options
.iter()
.find(|o| o.matches_long("verbose"))
.unwrap();
assert!(opt.description.contains("field doc"));
}
struct GitApp {
verbose: bool,
}
impl runi_cli::Command for GitApp {
fn schema() -> CommandSchema {
CommandSchema::new("git", "VCS").flag("-v,--verbose", "Verbose")
}
fn from_parsed(p: &ParseResult) -> Result<Self> {
Ok(Self {
verbose: p.flag("--verbose"),
})
}
}
#[derive(CommandDerive, Clone)]
#[command(description = "Initialize a repository")]
struct InitCmd {
#[argument]
dir: Option<String>,
}
impl SubCommandOf<GitApp> for InitCmd {
fn run(&self, _g: &GitApp) -> Result<()> {
Ok(())
}
}
#[derive(CommandDerive, Clone)]
#[command(description = "Clone a repository")]
struct CloneCmd {
#[argument]
url: String,
#[option("--depth")]
depth: Option<u32>,
}
impl SubCommandOf<GitApp> for CloneCmd {
fn run(&self, _g: &GitApp) -> Result<()> {
Ok(())
}
}
#[derive(CommandDerive)]
enum GitSub {
Init(InitCmd),
Clone(CloneCmd),
}
#[test]
fn enum_derive_register_on_launcher() {
let launcher = GitSub::register_on(Launcher::<GitApp>::of());
launcher
.run_args(&args(&["-v", "clone", "--depth", "1", "https://x"]))
.unwrap();
}
#[test]
fn enum_variant_doc_becomes_subcommand_description() {
let launcher = GitSub::register_on(Launcher::<GitApp>::of());
let schema = launcher.schema();
let init = schema
.subcommands
.iter()
.find(|s| s.name == "init")
.unwrap();
let clone = schema
.subcommands
.iter()
.find(|s| s.name == "clone")
.unwrap();
assert_eq!(init.description, "Initialize a repository.");
assert_eq!(clone.description, "Clone a repository.");
}
#[test]
fn command_with_description_overrides_inner_schema_description() {
let launcher =
Launcher::<GitApp>::of().command_with_description::<CloneCmd>("clone", "override!");
let schema = launcher.schema();
let clone = schema
.subcommands
.iter()
.find(|s| s.name == "clone")
.unwrap();
assert_eq!(clone.description, "override!");
}
#[test]
fn enum_derive_default_name_is_lowercase_variant() {
let launcher = GitSub::register_on(Launcher::<GitApp>::of());
launcher.run_args(&args(&["init"])).unwrap();
}
struct GreeterManual {
loud: bool,
target: String,
}
impl runi_cli::Command for GreeterManual {
fn schema() -> CommandSchema {
CommandSchema::new("greet", "Say hello.")
.flag("-l,--loud", "Shout instead of whisper.")
.argument("target", "Who to greet.")
}
fn from_parsed(p: &ParseResult) -> Result<Self> {
Ok(Self {
loud: p.flag("--loud"),
target: p.require::<String>("target")?,
})
}
}
#[test]
fn derive_and_manual_schemas_match() {
let derived = <Greeter as runi_cli::Command>::schema();
let manual = <GreeterManual as runi_cli::Command>::schema();
assert_eq!(derived.name, manual.name);
assert_eq!(derived.description, manual.description);
assert_eq!(derived.options.len(), manual.options.len());
assert_eq!(derived.arguments.len(), manual.arguments.len());
for (a, b) in derived.options.iter().zip(manual.options.iter()) {
assert_eq!(a.short, b.short);
assert_eq!(a.long, b.long);
assert_eq!(a.description, b.description);
assert_eq!(a.takes_value, b.takes_value);
}
for (a, b) in derived.arguments.iter().zip(manual.arguments.iter()) {
assert_eq!(a.name, b.name);
assert_eq!(a.description, b.description);
assert_eq!(a.required, b.required);
}
}
#[test]
fn derived_missing_required_reports_error() {
let launcher = Launcher::<Greeter>::of();
let err = launcher.parse(&args(&["--loud"])).unwrap_err();
assert!(matches!(err, Error::MissingArgument(ref n) if n == "target"));
}
#[derive(CommandDerive)]
#[command(name = "ping")]
struct Ping;
impl Runnable for Ping {
fn run(&self) -> Result<()> {
Ok(())
}
}
#[test]
fn unit_struct_derive() {
let schema = <Ping as runi_cli::Command>::schema();
assert_eq!(schema.name, "ping");
assert!(schema.options.is_empty());
assert!(schema.arguments.is_empty());
let parsed = OptionParser::parse(&schema, &args(&[])).unwrap();
let _: Ping = <Ping as runi_cli::Command>::from_parsed(&parsed).unwrap();
}