use std::{collections::HashMap, option::Option, sync::Arc};
use super::strategy::FallbackSubcommandStrategy;
use super::{
CommandStrategy, FunctionStrategy, StrategyError, SubcommandCatalog, SubcommandRouter,
};
#[derive(Clone)]
pub struct Switch {
pub name: String,
pub description: String,
pub aliases: Vec<String>,
}
impl Switch {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
aliases: Vec::new(),
}
}
pub fn with_aliases(mut self, aliases: Vec<String>) -> Self {
self.aliases = aliases;
self
}
}
#[derive(Clone)]
pub struct Argument {
pub name: String,
pub description: String,
pub aliases: Vec<String>,
pub value: Option<String>,
pub required: bool,
}
impl Argument {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
aliases: Vec::new(),
value: None,
required: false,
}
}
pub fn with_aliases(mut self, aliases: Vec<impl Into<String>>) -> Self {
self.aliases = aliases.into_iter().map(|s| s.into()).collect();
self
}
pub fn set_value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
pub fn set_required(mut self) -> Self {
self.required = true;
self
}
}
#[derive(Clone)]
pub struct CommandMetaData {
pub name: String,
pub description: String,
pub usage: Option<String>,
pub long_description: Option<String>,
pub examples: Vec<String>,
pub options: Vec<Switch>,
pub arguments: Vec<Argument>,
pub aliases: Vec<String>,
}
impl CommandMetaData {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
usage: None,
long_description: None,
examples: Vec::new(),
options: Vec::new(),
arguments: Vec::new(),
aliases: Vec::new(),
}
}
pub fn with_usage(mut self, usage: impl Into<String>) -> Self {
self.usage = Some(usage.into());
self
}
pub fn with_long_description(mut self, long_description: impl Into<String>) -> Self {
self.long_description = Some(long_description.into());
self
}
pub fn with_examples(mut self, examples: Vec<String>) -> Self {
self.examples = examples;
self
}
pub fn with_options(mut self, options: Vec<Switch>) -> Self {
self.options = options;
self
}
pub fn with_arguments(mut self, arguments: Vec<Argument>) -> Self {
self.arguments = arguments;
self
}
pub fn with_aliases(mut self, aliases: Vec<String>) -> Self {
self.aliases = aliases;
self
}
}
#[derive(Clone)]
pub struct Command {
pub metadata: CommandMetaData,
strategy: Arc<dyn CommandStrategy>,
}
impl Command {
pub fn new<S>(name: impl Into<String>, description: impl Into<String>, strategy: S) -> Self
where
S: CommandStrategy + 'static,
{
Self {
metadata: CommandMetaData::new(name, description),
strategy: Arc::new(strategy),
}
}
pub fn execute(&self, args: Vec<String>) -> Result<(), StrategyError> {
let invocation = parser::ArgumentParser::parse(args, &self.metadata, |token| {
self.matches_subcommand(token)
})?;
self.strategy.execute(
invocation.options,
invocation.arguments,
invocation.subcommands,
)
}
pub fn subcommand_catalog(&self) -> Option<&dyn SubcommandCatalog> {
self.strategy.subcommand_catalog()
}
pub fn from_fn<F>(name: impl Into<String>, description: impl Into<String>, runner: F) -> Self
where
F: Fn(Vec<String>, HashMap<String, String>, Vec<String>) -> Result<(), StrategyError>
+ Send
+ Sync
+ 'static,
{
Self::new(name, description, FunctionStrategy::new(runner))
}
pub fn builder(name: impl Into<String>, description: impl Into<String>) -> CommandBuilder {
CommandBuilder::new(name, description)
}
fn matches_subcommand(&self, token: &str) -> bool {
self.subcommand_catalog().is_some_and(|catalog| {
catalog.subcommands().into_iter().any(|command| {
command.metadata.name == token
|| command.metadata.aliases.iter().any(|alias| alias == token)
})
})
}
}
struct ParsedInvocation {
options: Vec<String>,
arguments: HashMap<String, String>,
subcommands: Vec<String>,
}
pub fn command(name: impl Into<String>, description: impl Into<String>) -> CommandBuilder {
CommandBuilder::new(name, description)
}
pub fn argument(name: impl Into<String>, description: impl Into<String>) -> Argument {
Argument::new(name, description)
}
pub fn switch(name: impl Into<String>, description: impl Into<String>) -> Switch {
Switch::new(name, description)
}
pub struct CommandBuilder {
metadata: CommandMetaData,
strategy: Option<Arc<dyn CommandStrategy>>,
subcommands: Vec<Command>,
}
impl CommandBuilder {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
metadata: CommandMetaData::new(name, description),
strategy: None,
subcommands: Vec::new(),
}
}
pub fn handler<S>(mut self, strategy: S) -> Self
where
S: CommandStrategy + 'static,
{
self.strategy = Some(Arc::new(strategy));
self
}
pub fn handler_fn<F>(mut self, runner: F) -> Self
where
F: Fn(Vec<String>, HashMap<String, String>, Vec<String>) -> Result<(), StrategyError>
+ Send
+ Sync
+ 'static,
{
self.strategy = Some(Arc::new(FunctionStrategy::new(runner)));
self
}
pub fn subcommand<C>(mut self, subcommand: C) -> Self
where
C: Into<Command>,
{
self.subcommands.push(subcommand.into());
self
}
pub fn with_usage(mut self, usage: impl Into<String>) -> Self {
self.metadata = self.metadata.with_usage(usage);
self
}
pub fn with_long_description(mut self, long_description: impl Into<String>) -> Self {
self.metadata = self.metadata.with_long_description(long_description);
self
}
pub fn with_examples(mut self, examples: Vec<String>) -> Self {
self.metadata = self.metadata.with_examples(examples);
self
}
pub fn with_options(mut self, options: Vec<Switch>) -> Self {
self.metadata = self.metadata.with_options(options);
self
}
pub fn with_arguments(mut self, arguments: Vec<Argument>) -> Self {
self.metadata = self.metadata.with_arguments(arguments);
self
}
pub fn with_aliases(mut self, aliases: Vec<impl Into<String>>) -> Self {
self.metadata = self
.metadata
.with_aliases(aliases.into_iter().map(|s| s.into()).collect());
self
}
pub fn build(self) -> Command {
let strategy: Arc<dyn CommandStrategy> = if self.subcommands.is_empty() {
self.strategy.unwrap_or_else(|| {
Arc::new(FunctionStrategy::new(|_, _, _| {
Err(StrategyError::internal(
"command has no handler; configure a handler or subcommand",
))
}))
})
} else {
let mut router = SubcommandRouter::new();
for subcommand in self.subcommands {
router.register_mut(subcommand);
}
match self.strategy {
Some(fallback) => Arc::new(FallbackSubcommandStrategy::new(fallback, router)),
None => Arc::new(router),
}
};
Command {
metadata: self.metadata,
strategy,
}
}
}
impl From<CommandBuilder> for Command {
fn from(value: CommandBuilder) -> Self {
value.build()
}
}
mod parser {
use super::{CommandMetaData, ParsedInvocation};
use crate::StrategyError;
pub(super) struct ArgumentParser;
impl ArgumentParser {
pub fn parse<F>(
args: Vec<String>,
metadata: &CommandMetaData,
is_subcommand: F,
) -> Result<ParsedInvocation, StrategyError>
where
F: Fn(&str) -> bool,
{
let mut options = Vec::new();
let mut arguments = std::collections::HashMap::new();
let mut index = 0;
let has_declared_inputs =
!metadata.options.is_empty() || !metadata.arguments.is_empty();
while index < args.len() {
let token = &args[index];
if is_subcommand(token) {
break;
}
let Some(flag) = token.strip_prefix("--") else {
return Err(StrategyError::invalid_arguments(format!(
"unexpected argument '{token}'. positional arguments must use a flag before subcommands"
)));
};
if let Some(name) = metadata
.arguments
.iter()
.find(|argument| {
argument.name == flag || argument.aliases.iter().any(|alias| alias == flag)
})
.map(|argument| argument.name.clone())
{
options.push(name);
index += 1;
continue;
}
if let Some(option) = metadata
.options
.iter()
.find(|option| {
option.name == flag || option.aliases.iter().any(|alias| alias == flag)
})
.cloned()
{
if let Some((_, value)) = flag.split_once('=') {
arguments.insert(option.name, value.to_string());
index += 1;
continue;
}
let Some(next) = args.get(index + 1) else {
return Err(StrategyError::invalid_arguments(format!(
"missing value for option '--{}'",
option.name
)));
};
if next.starts_with("--") || is_subcommand(next) {
return Err(StrategyError::invalid_arguments(format!(
"missing value for option '--{}'",
option.name
)));
}
arguments.insert(option.name, next.clone());
index += 2;
continue;
}
if let Some((key, value)) = flag.split_once('=') {
arguments.insert(key.to_string(), value.to_string());
index += 1;
continue;
}
if has_declared_inputs {
return Err(StrategyError::invalid_arguments(format!(
"unknown flag '--{flag}'"
)));
}
if let Some(next) = args.get(index + 1) {
if next.starts_with("--") || is_subcommand(next) {
options.push(flag.to_string());
index += 1;
} else {
arguments.insert(flag.to_string(), next.clone());
index += 2;
}
} else {
options.push(flag.to_string());
index += 1;
}
}
Ok(ParsedInvocation {
options,
arguments,
subcommands: args[index..].to_vec(),
})
}
}
}