use std::sync::Arc;
use rainier_container::Application;
use rainier_support::Result;
use crate::arguments::Arguments;
pub mod exit {
pub const SUCCESS: i32 = 0;
pub const FAILURE: i32 = 1;
pub const USAGE: i32 = 2;
}
#[async_trait::async_trait]
pub trait Command: Send + Sync + 'static {
fn name(&self) -> &str;
fn description(&self) -> &str {
""
}
fn help(&self) -> Option<&str> {
None
}
async fn handle(&self, args: &Arguments, app: &Application) -> Result<i32>;
}
pub struct Console {
name: String,
commands: Vec<Arc<dyn Command>>,
}
impl Console {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into(), commands: Vec::new() }
}
pub fn register(mut self, command: impl Command) -> Self {
self.commands.push(Arc::new(command));
self
}
pub fn register_arc(mut self, command: Arc<dyn Command>) -> Self {
self.commands.push(command);
self
}
pub fn commands(&self) -> Vec<Arc<dyn Command>> {
let mut commands = self.commands.clone();
commands.sort_by(|a, b| a.name().cmp(b.name()));
commands
}
pub fn find(&self, name: &str) -> Option<Arc<dyn Command>> {
self.commands.iter().find(|command| command.name() == name).cloned()
}
pub async fn run(&self, app: &Application, args: Arguments) -> i32 {
if args.is_empty() {
println!("{}", self.usage());
return exit::SUCCESS;
}
if args.command() == "list" {
println!("{}", self.usage());
return exit::SUCCESS;
}
if args.command() == "help" {
return match args.argument(0) {
Some(name) => self.print_help(name),
None => {
println!("{}", self.usage());
exit::SUCCESS
}
};
}
let Some(command) = self.find(args.command()) else {
eprintln!("Command `{}` is not defined.", args.command());
if let Some(suggestion) = self.closest(args.command()) {
eprintln!("Did you mean `{suggestion}`?");
}
eprintln!("\nRun `{} list` to see what is available.", self.name);
return exit::USAGE;
};
if args.flag("help") || args.flag("h") {
return self.print_help(command.name());
}
match command.handle(&args, app).await {
Ok(code) => code,
Err(e) => {
eprintln!("{}: {e}", command.name());
exit::FAILURE
}
}
}
pub async fn run_argv<I, S>(&self, app: &Application, argv: I) -> i32
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.run(app, Arguments::parse(argv)).await
}
pub async fn run_from_env(&self, app: &Application) -> i32 {
self.run_argv(app, std::env::args().skip(1)).await
}
pub fn usage(&self) -> String {
use std::fmt::Write as _;
let mut out = String::new();
let _ = writeln!(out, "{}\n", self.name);
let _ = writeln!(out, "Usage:\n {} <command> [options]\n", self.name);
if self.commands.is_empty() {
let _ = writeln!(out, "No commands are registered.");
return out;
}
let width = self.commands.iter().map(|c| c.name().len()).max().unwrap_or(0);
let _ = writeln!(out, "Available commands:");
for command in self.commands() {
let _ = writeln!(
out,
" {:width$} {}",
command.name(),
command.description(),
width = width
);
}
out
}
fn print_help(&self, name: &str) -> i32 {
let Some(command) = self.find(name) else {
eprintln!("Command `{name}` is not defined.");
return exit::USAGE;
};
println!("{}\n", command.name());
if !command.description().is_empty() {
println!("{}\n", command.description());
}
match command.help() {
Some(help) => println!("{help}"),
None => println!("Usage:\n {} {name} [options]", self.name),
}
exit::SUCCESS
}
fn closest(&self, name: &str) -> Option<String> {
let lower = name.to_lowercase();
self.commands()
.iter()
.map(|command| command.name().to_string())
.filter(|candidate| {
let candidate = candidate.to_lowercase();
candidate.starts_with(&lower)
|| lower.starts_with(&candidate)
|| candidate.contains(&lower)
|| lower.contains(&candidate)
})
.min_by_key(|candidate| candidate.len())
}
}
impl std::fmt::Debug for Console {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Console")
.field("name", &self.name)
.field(
"commands",
&self.commands().iter().map(|c| c.name().to_string()).collect::<Vec<_>>(),
)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rainier_support::Error;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
static RAN: Mutex<Vec<String>> = Mutex::new(Vec::new());
static CALLS: AtomicUsize = AtomicUsize::new(0);
struct Greet;
#[async_trait::async_trait]
impl Command for Greet {
fn name(&self) -> &str {
"greet"
}
fn description(&self) -> &str {
"Say hello"
}
async fn handle(&self, args: &Arguments, _: &Application) -> Result<i32> {
CALLS.fetch_add(1, Ordering::SeqCst);
RAN.lock().unwrap().push(args.argument(0).unwrap_or("world").to_string());
Ok(exit::SUCCESS)
}
}
struct Failing;
#[async_trait::async_trait]
impl Command for Failing {
fn name(&self) -> &str {
"queue:work"
}
fn description(&self) -> &str {
"Process queued jobs"
}
async fn handle(&self, _: &Arguments, _: &Application) -> Result<i32> {
Err(Error::internal("the queue is unreachable"))
}
}
struct NonZero;
#[async_trait::async_trait]
impl Command for NonZero {
fn name(&self) -> &str {
"check"
}
async fn handle(&self, _: &Arguments, _: &Application) -> Result<i32> {
Ok(3)
}
}
fn console() -> Console {
Console::new("rainier").register(Greet).register(Failing).register(NonZero)
}
fn app() -> Application {
Application::new(".")
}
async fn run(line: &str) -> i32 {
console().run_argv(&app(), line.split_whitespace()).await
}
#[tokio::test]
async fn runs_a_registered_command() {
RAN.lock().unwrap().clear();
assert_eq!(run("greet Ada").await, exit::SUCCESS);
assert_eq!(*RAN.lock().unwrap(), vec!["Ada"]);
}
#[tokio::test]
async fn a_commands_exit_code_is_returned() {
assert_eq!(run("check").await, 3);
}
#[tokio::test]
async fn an_erroring_command_reports_a_failure_rather_than_propagating() {
assert_eq!(run("queue:work").await, exit::FAILURE);
}
#[tokio::test]
async fn an_unknown_command_is_a_usage_error() {
assert_eq!(run("nonsense").await, exit::USAGE);
}
#[tokio::test]
async fn no_command_prints_the_list() {
assert_eq!(console().run_argv(&app(), Vec::<String>::new()).await, exit::SUCCESS);
assert_eq!(run("list").await, exit::SUCCESS);
}
#[tokio::test]
async fn help_is_available_for_a_command() {
assert_eq!(run("help greet").await, exit::SUCCESS);
assert_eq!(run("help nonsense").await, exit::USAGE);
assert_eq!(run("help").await, exit::SUCCESS);
}
#[tokio::test]
async fn a_help_flag_shows_help_instead_of_running() {
let before = CALLS.load(Ordering::SeqCst);
assert_eq!(run("greet --help").await, exit::SUCCESS);
assert_eq!(CALLS.load(Ordering::SeqCst), before, "the command must not have run");
}
#[test]
fn the_usage_text_lists_every_command_aligned() {
let usage = console().usage();
assert!(usage.contains("Available commands:"), "{usage}");
assert!(usage.contains("greet"), "{usage}");
assert!(usage.contains("Say hello"), "{usage}");
assert!(usage.contains("queue:work"), "{usage}");
assert!(
usage.find("check").unwrap() < usage.find("greet").unwrap(),
"commands should be sorted: {usage}"
);
}
#[test]
fn an_empty_console_says_so() {
assert!(Console::new("rainier").usage().contains("No commands"));
}
#[test]
fn a_near_miss_is_suggested() {
let console = console();
assert_eq!(console.closest("greet2").as_deref(), Some("greet"));
assert_eq!(console.closest("gree").as_deref(), Some("greet"));
assert_eq!(console.closest("queue").as_deref(), Some("queue:work"));
assert_eq!(console.closest("zzz"), None);
}
#[test]
fn commands_can_be_looked_up() {
let console = console();
assert!(console.find("greet").is_some());
assert!(console.find("nope").is_none());
assert_eq!(console.commands().len(), 3);
}
}