use std::{option::Option, sync::Arc};
use super::strategy::FallbackSubcommandStrategy;
use super::{
CommandStrategy, FunctionStrategy, StrategyError, SubcommandCatalog, SubcommandRouter,
};
#[derive(Clone, Debug, PartialEq, Eq)]
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, Debug, PartialEq, Eq)]
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.params)
}
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<Switch>, Vec<Argument>, 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<Switch>,
arguments: Vec<Argument>,
params: 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<Switch>, Vec<Argument>, 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::{Argument, CommandMetaData, ParsedInvocation, Switch};
use crate::StrategyError;
pub(super) struct ArgumentParser;
impl ArgumentParser {
fn find_declared_argument<'a>(
metadata: &'a CommandMetaData,
flag: &str,
) -> Option<&'a Argument> {
metadata.arguments.iter().find(|argument| {
argument.name == flag || argument.aliases.iter().any(|alias| alias == flag)
})
}
fn find_declared_switch<'a>(
metadata: &'a CommandMetaData,
flag: &str,
) -> Option<&'a Switch> {
metadata.options.iter().find(|option| {
option.name == flag || option.aliases.iter().any(|alias| alias == flag)
})
}
fn upsert_argument(arguments: &mut Vec<Argument>, argument: Argument) {
if let Some(existing) = arguments
.iter_mut()
.find(|existing| existing.name == argument.name)
{
*existing = argument;
return;
}
arguments.push(argument);
}
fn validate_required_arguments(
metadata: &CommandMetaData,
arguments: &[Argument],
) -> Result<(), StrategyError> {
for required in metadata
.arguments
.iter()
.filter(|argument| argument.required)
{
let value = arguments
.iter()
.find(|argument| argument.name == required.name)
.and_then(|argument| argument.value.as_deref());
if value.is_none_or(|value| value.trim().is_empty()) {
return Err(StrategyError::invalid_arguments(format!(
"missing value for required argument '--{}'",
required.name
)));
}
}
Ok(())
}
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 = Vec::new();
let mut params = Vec::new();
let mut index = 0;
while index < args.len() {
let token = &args[index];
if is_subcommand(token) {
break;
}
let Some(flag) = token.strip_prefix("--") else {
params.push(token.clone());
index += 1;
continue;
};
if let Some((flag_name, inline_value)) = flag.split_once('=') {
if let Some(argument_decl) = Self::find_declared_argument(metadata, flag_name) {
Self::upsert_argument(
&mut arguments,
argument_decl.clone().set_value(inline_value.to_string()),
);
index += 1;
continue;
}
if let Some(option_decl) = Self::find_declared_switch(metadata, flag_name) {
return Err(StrategyError::invalid_arguments(format!(
"switch '--{}' does not take a value",
option_decl.name
)));
}
return Err(StrategyError::invalid_arguments(format!(
"unknown flag '--{}'",
flag_name
)));
}
if let Some(argument_decl) = Self::find_declared_argument(metadata, flag) {
let Some(next) = args.get(index + 1) else {
return Err(StrategyError::invalid_arguments(format!(
"missing value for argument '--{}'",
argument_decl.name
)));
};
if next.starts_with("--") || is_subcommand(next) {
return Err(StrategyError::invalid_arguments(format!(
"missing value for argument '--{}'",
argument_decl.name
)));
}
Self::upsert_argument(
&mut arguments,
argument_decl.clone().set_value(next.clone()),
);
index += 2;
continue;
}
if let Some(option_decl) = Self::find_declared_switch(metadata, flag) {
if flag.contains('=') {
return Err(StrategyError::invalid_arguments(format!(
"switch '--{}' does not take a value",
option_decl.name
)));
}
options.push(option_decl.clone());
index += 1;
continue;
}
return Err(StrategyError::invalid_arguments(format!(
"unknown flag '--{}'",
flag
)));
}
params.extend_from_slice(&args[index..]);
Self::validate_required_arguments(metadata, &arguments)?;
Ok(ParsedInvocation {
options,
arguments,
params,
})
}
}
}