use super::{Body, Task};
struct Frame<'a> {
command: clap::Command,
remaining: std::slice::Iter<'a, (&'static str, Task)>,
}
pub(super) fn declare_tree(task: &Task, name: &'static str) -> clap::Command {
let children = match &task.body {
Body::Handler { augment, .. } => return declare_leaf(*augment, task.about, name),
Body::Children(children) => children,
};
let root = clap::Command::new(name)
.about(task.about)
.subcommand_required(true)
.arg_required_else_help(true);
let mut stack = vec![Frame {
command: root,
remaining: children.iter(),
}];
loop {
let Some(frame) = stack.last_mut() else {
unreachable!("declare_tree's stack is never left empty without returning first");
};
let Some((child_name, child_task)) = frame.remaining.next() else {
let finished = stack
.pop()
.unwrap_or_else(|| unreachable!("just matched Some(frame) on this same stack"));
let Some(parent) = stack.last_mut() else {
return finished.command;
};
parent.command = std::mem::take(&mut parent.command).subcommand(finished.command);
continue;
};
match &child_task.body {
Body::Handler { augment, .. } => {
let child_command = declare_leaf(*augment, child_task.about, child_name);
frame.command = std::mem::take(&mut frame.command).subcommand(child_command);
}
Body::Children(grandchildren) => {
let child_command = clap::Command::new(*child_name)
.about(child_task.about)
.subcommand_required(true)
.arg_required_else_help(true);
stack.push(Frame {
command: child_command,
remaining: grandchildren.iter(),
});
}
}
}
}
fn declare_leaf(
augment: fn(clap::Command) -> clap::Command,
about: &'static str,
name: &'static str,
) -> clap::Command {
augment(clap::Command::new(name))
.about(about)
.long_about(None::<&str>)
}
#[cfg(test)]
mod tests {
use super::declare_tree;
use crate::task::Task;
use crate::test_support::run_ok;
#[test]
fn declare_tree_on_a_leaf_is_unchanged_from_a_plain_command() {
let task = Task::new("say hello", run_ok);
let command = declare_tree(&task, "greet");
assert_eq!(command.get_name(), "greet");
assert_eq!(
command.get_about().map(ToString::to_string),
Some("say hello".to_string())
);
assert_eq!(command.get_subcommands().count(), 0);
}
#[test]
fn declare_tree_on_a_group_lists_its_children_in_order_with_their_own_about() {
let bundle = Task::group(
"a bundle",
[
("first", Task::new("the first child", run_ok)),
("second", Task::new("the second child", run_ok)),
],
);
let command = declare_tree(&bundle, "bundle");
let names: Vec<&str> = command
.get_subcommands()
.map(clap::Command::get_name)
.collect();
assert_eq!(names, ["first", "second"]);
let abouts: Vec<Option<String>> = command
.get_subcommands()
.map(|subcommand| subcommand.get_about().map(ToString::to_string))
.collect();
assert_eq!(
abouts,
[
Some("the first child".to_string()),
Some("the second child".to_string()),
]
);
}
fn documented_command() -> clap::Command {
#[derive(clap::Args)]
struct TwoParagraphArguments {
#[arg(long)]
value: Option<String>,
}
let task = Task::new(
"the task's own about",
|_arguments: TwoParagraphArguments| Ok(()),
);
declare_tree(&task, "greet")
}
#[test]
fn a_leafs_help_describes_the_task_and_never_its_argument_structs_doc() {
let task_about = "the task's own about";
for (label, render) in [
(
"-h",
clap::Command::render_help as fn(&mut clap::Command) -> _,
),
("--help", clap::Command::render_long_help),
] {
let mut command = documented_command();
let rendered = render(&mut command).to_string();
assert_eq!(
rendered.lines().next(),
Some(task_about),
"expected `{label}` to open with the task's about and nothing else; \
it was:\n{rendered}"
);
for leaked in [
"first paragraph",
"second paragraph",
"argument struct's doc",
] {
assert!(
!rendered.contains(leaked),
"expected `{label}` not to carry the argument struct's doc ({leaked:?}); \
it was:\n{rendered}"
);
}
}
}
#[test]
fn declare_tree_on_a_two_level_group_puts_the_grandchild_under_the_child() {
let inner = Task::group("inner bundle", [("leaf", Task::new("a leaf", run_ok))]);
let outer = Task::group("outer bundle", [("inner", inner)]);
let command = declare_tree(&outer, "outer");
let top_names: Vec<&str> = command
.get_subcommands()
.map(clap::Command::get_name)
.collect();
assert_eq!(top_names, ["inner"]);
let inner_command = command
.get_subcommands()
.find(|subcommand| subcommand.get_name() == "inner");
assert!(inner_command.is_some(), "expected an `inner` subcommand");
if let Some(inner_command) = inner_command {
let grandchild_names: Vec<&str> = inner_command
.get_subcommands()
.map(clap::Command::get_name)
.collect();
assert_eq!(grandchild_names, ["leaf"]);
}
}
}