use crate::name::Name;
use crate::outcome::Failure;
use crate::task::Task;
use super::{refusal_line, report_to_stderr};
fn mount_refusal(name: &str) -> String {
format!(
"`{name}` collides with an earlier import, or is not a usable name; dropped from this \
command line — run `regenerate` to fix its generated file"
)
}
pub(super) fn assemble(
binary_name: &str,
imported: impl IntoIterator<Item = (&'static str, Task)>,
) -> Vec<(&'static str, Task)> {
let mut tasks = Vec::new();
for (name, task) in imported {
let already_present = tasks.iter().any(|(existing, _)| *existing == name);
let usable = Name::new(name).is_ok();
if already_present || !usable {
report_to_stderr(refusal_line(binary_name, mount_refusal(name)));
continue;
}
tasks.push((name, task));
}
tasks
}
#[derive(Debug)]
pub(super) struct TopLevel {
pub(super) mounts: Vec<(&'static str, Task)>,
pub(super) flattened: Vec<&'static str>,
}
pub(super) fn flatten(
binary_name: &str,
mounts: Vec<(&'static str, Task)>,
) -> Result<TopLevel, Failure> {
let bin_name_index = mounts.iter().position(|(key, _)| *key == binary_name);
let (mounts, flattened) = match bin_name_index {
None => (mounts, Vec::new()),
Some(index) => {
let mut mounts = mounts;
let (_, mount) = mounts.remove(index);
let children = mount
.into_children()
.map_err(|_leaf| leaf_under_bin_name_refusal(binary_name))?;
ensure_no_flatten_collision(binary_name, &mounts, &children)?;
let flattened: Vec<&'static str> = children.iter().map(|(name, _)| *name).collect();
mounts.splice(index..index, children);
(mounts, flattened)
}
};
ensure_no_top_level_help(&mounts)?;
Ok(TopLevel { mounts, flattened })
}
fn leaf_under_bin_name_refusal(bin_name: &str) -> Failure {
Failure::new(format!(
"`{bin_name}` is mounted under `{bin_name}`, the name this command line was built as, \
so it is this command line's top level — but it is not a bundle. Make it one with \
`Task::group`, or mount it under another name."
))
}
fn ensure_no_flatten_collision(
bin_name: &str,
surrounding_mounts: &[(&'static str, Task)],
children: &[(&'static str, Task)],
) -> Result<(), Failure> {
for (child_name, _) in children {
if surrounding_mounts
.iter()
.any(|(mount_name, _)| mount_name == child_name)
{
return Err(flatten_collision_refusal(bin_name, child_name));
}
}
Ok(())
}
fn flatten_collision_refusal(bin_name: &str, name: &str) -> Failure {
Failure::new(format!(
"`{name}` would be a top-level command of this command line twice: once from the \
bundle mounted under `{bin_name}`, the name this command line was built as, and once \
mounted under `{name}` itself. Rename the bundle's child, or mount that task under \
another name."
))
}
fn ensure_no_top_level_help(mounts: &[(&'static str, Task)]) -> Result<(), Failure> {
if mounts.iter().any(|(key, _)| *key == "help") {
return Err(help_collision_refusal());
}
Ok(())
}
fn help_collision_refusal() -> Failure {
Failure::new(
"`help` would be a top-level command of this command line twice: once as this mount, \
and once as the `help` command clap gives every command that has subcommands. Mount \
it under another name.",
)
}
#[cfg(test)]
mod tests {
use super::{assemble, flatten, mount_refusal};
use crate::dispatch::refusal_line;
use crate::task::Task;
use crate::test_support::run_ok;
fn task() -> Task {
Task::new("a test task", run_ok)
}
#[test]
fn imports_are_assembled_in_order() {
let imported = vec![("regenerate", task()), ("greet", task())];
let assembled = assemble("demo", imported);
let names: Vec<&str> = assembled.iter().map(|(name, _)| *name).collect();
assert_eq!(names, ["regenerate", "greet"]);
}
#[test]
fn an_import_colliding_with_an_earlier_import_is_dropped() {
let imported = vec![("greet", task()), ("greet", task())];
let assembled = assemble("demo", imported);
let names: Vec<&str> = assembled.iter().map(|(name, _)| *name).collect();
assert_eq!(names, ["greet"]);
}
#[test]
fn an_import_with_an_invalid_name_is_dropped() {
let imported = vec![("regenerate", task()), ("../evil", task())];
let assembled = assemble("demo", imported);
let names: Vec<&str> = assembled.iter().map(|(name, _)| *name).collect();
assert_eq!(names, ["regenerate"]);
}
#[test]
fn a_task_named_add_mounts_like_any_other_import() {
let imported = vec![("add", task())];
let assembled = assemble("demo", imported);
let names: Vec<&str> = assembled.iter().map(|(name, _)| *name).collect();
assert_eq!(names, ["add"]);
}
#[test]
fn a_mount_refusal_leads_with_the_bin_name() {
let line = refusal_line("ritual", mount_refusal("new"));
assert!(line.starts_with("ritual: "));
assert!(line.contains("`new`"));
assert!(line.contains("collides"));
}
fn names_of(mounts: &[(&'static str, Task)]) -> Vec<&'static str> {
mounts.iter().map(|(name, _)| *name).collect()
}
#[test]
fn flatten_with_no_bin_name_mount_is_the_identity() {
let mounts = vec![("add", task()), ("regenerate", task())];
let result = flatten("acme", mounts);
assert!(result.is_ok(), "expected no refusal: {result:?}");
if let Ok(top_level) = result {
assert_eq!(names_of(&top_level.mounts), ["add", "regenerate"]);
assert!(
top_level.flattened.is_empty(),
"expected nothing to have been promoted: {:?}",
top_level.flattened
);
}
}
#[test]
fn flatten_splices_in_place_and_preserves_surrounding_order() {
let bundle = Task::group("acme's own commands", [("add", task()), ("build", task())]);
let mounts = vec![("lint", task()), ("acme", bundle), ("format", task())];
let result = flatten("acme", mounts);
assert!(result.is_ok(), "expected no refusal: {result:?}");
if let Ok(top_level) = result {
assert_eq!(
names_of(&top_level.mounts),
["lint", "add", "build", "format"],
"the bundle's children replace it in place, in their own order, and every \
surrounding mount stays exactly where it was"
);
}
}
#[test]
fn flatten_reports_the_names_it_promoted() {
let bundle = Task::group("acme's own commands", [("add", task()), ("build", task())]);
let mounts = vec![("acme", bundle)];
let result = flatten("acme", mounts);
assert!(result.is_ok(), "expected no refusal: {result:?}");
if let Ok(top_level) = result {
assert_eq!(top_level.flattened, ["add", "build"]);
}
}
#[test]
fn a_leaf_under_the_bin_name_is_refused() {
let mounts = vec![("acme", task())];
let result = flatten("acme", mounts);
assert!(result.is_err(), "expected a refusal: {result:?}");
if let Err(failure) = result {
let message = failure.to_string();
assert!(message.contains("`acme`"), "message was: {message}");
assert!(message.contains("Task::group"), "message was: {message}");
}
}
#[test]
fn a_child_colliding_with_a_later_mount_key_is_refused() {
let bundle = Task::group("acme's own commands", [("lint", task())]);
let mounts = vec![("acme", bundle), ("lint", task())];
let result = flatten("acme", mounts);
assert!(result.is_err(), "expected a refusal: {result:?}");
if let Err(failure) = result {
let message = failure.to_string();
assert!(message.contains("`lint`"), "message was: {message}");
assert!(message.contains("`acme`"), "message was: {message}");
assert!(message.contains("another name"), "message was: {message}");
}
}
#[test]
fn a_child_colliding_with_an_earlier_mount_key_is_refused() {
let bundle = Task::group("acme's own commands", [("lint", task())]);
let mounts = vec![("lint", task()), ("acme", bundle)];
let result = flatten("acme", mounts);
assert!(result.is_err(), "expected a refusal: {result:?}");
if let Err(failure) = result {
let message = failure.to_string();
assert!(message.contains("`lint`"), "message was: {message}");
}
}
#[test]
fn a_child_named_the_same_as_the_bin_is_mounted_once_not_flattened_again() {
let bundle = Task::group("acme's own commands", [("acme", task())]);
let mounts = vec![("acme", bundle)];
let result = flatten("acme", mounts);
assert!(result.is_ok(), "expected no refusal: {result:?}");
if let Ok(top_level) = result {
assert_eq!(names_of(&top_level.mounts), ["acme"]);
}
}
#[test]
fn a_nested_bundle_under_a_key_equal_to_the_bin_name_is_untouched() {
let inner = Task::group("nested, keyed the same as the bin", [("lint", task())]);
let outer = Task::group("an ordinary bundle", [("acme", inner)]);
let mounts = vec![("outer", outer)];
let result = flatten("acme", mounts);
assert!(result.is_ok(), "expected no refusal: {result:?}");
if let Ok(top_level) = result {
assert_eq!(names_of(&top_level.mounts), ["outer"]);
}
}
#[test]
fn a_mount_key_named_help_is_refused() {
let mounts = vec![("help", task())];
let result = flatten("acme", mounts);
assert!(result.is_err(), "expected a refusal: {result:?}");
if let Err(failure) = result {
let message = failure.to_string();
assert!(message.contains("`help`"), "message was: {message}");
assert!(message.contains("clap"), "message was: {message}");
}
}
#[test]
fn a_help_mount_alongside_a_flattened_bundle_is_refused() {
let bundle = Task::group("acme's own commands", [("lint", task())]);
let mounts = vec![("acme", bundle), ("help", task())];
let result = flatten("acme", mounts);
assert!(result.is_err(), "expected a refusal: {result:?}");
if let Err(failure) = result {
let message = failure.to_string();
assert!(message.contains("`help`"), "message was: {message}");
assert!(message.contains("clap"), "message was: {message}");
}
}
}