use std::fmt;
use std::marker::PhantomData;
use std::sync::Arc;
pub use clap::{Arg, ArgMatches};
use rustclamp_core::{
Contribution, ContributionId, ContributionTarget, ContributionTargetId, ModuleId, Qualifier,
};
type Handler = Arc<dyn Fn(&ArgMatches) -> i32 + Send + Sync>;
pub struct CommandDeclaration<Q> {
command: clap::Command,
run: Handler,
qualifier: PhantomData<fn() -> Q>,
}
impl<Q> CommandDeclaration<Q> {
pub fn new(
name: &'static str,
description: &'static str,
run: impl Fn(&ArgMatches) -> i32 + Send + Sync + 'static,
) -> Self {
Self {
command: clap::Command::new(name).about(description),
run: Arc::new(run),
qualifier: PhantomData,
}
}
#[must_use]
pub fn arg(mut self, arg: impl Into<Arg>) -> Self {
self.command = self.command.arg(arg);
self
}
pub fn name(&self) -> &str {
self.command.get_name()
}
}
impl<Q: Qualifier> Contribution for CommandDeclaration<Q> {
const ID: ContributionId = ContributionId::new("rustclamp.cli.command");
}
pub struct CliCommandTarget<Q> {
name: &'static str,
qualifier: PhantomData<fn() -> Q>,
}
impl<Q> CliCommandTarget<Q> {
pub const fn new(name: &'static str) -> Self {
Self {
name,
qualifier: PhantomData,
}
}
}
impl<Q: Qualifier> ContributionTarget for CliCommandTarget<Q> {
type Contribution = CommandDeclaration<Q>;
type Runtime = CommandTree;
type Error = CliBuildError;
const ID: ContributionTargetId = ContributionTargetId::new("rustclamp.cli.commands");
fn build(
&self,
contributions: &[(ModuleId, Self::Contribution)],
) -> Result<CommandTree, CliBuildError> {
let mut sorted = contributions.iter().collect::<Vec<_>>();
sorted.sort_by(|(a_module, a), (b_module, b)| {
(a.name(), *a_module).cmp(&(b.name(), *b_module))
});
for pair in sorted.windows(2) {
if pair[0].1.name() == pair[1].1.name() {
return Err(CliBuildError {
target: Self::ID,
command: pair[0].1.name().to_owned(),
first_contributor: pair[0].0,
second_contributor: pair[1].0,
});
}
}
let command = clap::Command::new(self.name)
.subcommand_required(true)
.subcommands(sorted.iter().map(|(_, d)| d.command.clone()));
let handlers = sorted
.iter()
.map(|(_, d)| (d.command.get_name().to_owned(), Arc::clone(&d.run)))
.collect();
Ok(CommandTree { command, handlers })
}
}
pub struct CommandTree {
command: clap::Command,
handlers: Vec<(String, Handler)>,
}
impl CommandTree {
pub fn command(&self) -> &clap::Command {
&self.command
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.handlers.iter().map(|(name, _)| name.as_str())
}
pub fn run<I, T>(&self, args: I) -> Result<i32, clap::Error>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let matches = self.command.clone().try_get_matches_from(args)?;
let (name, sub) = matches
.subcommand()
.expect("subcommand_required makes clap reject a missing command");
let (_, run) = self
.handlers
.iter()
.find(|(n, _)| n == name)
.expect("every subcommand in the tree has a handler");
Ok(run(sub))
}
}
impl fmt::Debug for CommandTree {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list().entries(self.names()).finish()
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct CliBuildError {
pub target: ContributionTargetId,
pub command: String,
pub first_contributor: ModuleId,
pub second_contributor: ModuleId,
}
impl fmt::Display for CliBuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"target {} has duplicate command {:?} from {} and {}",
self.target.as_str(),
self.command,
self.first_contributor.as_str(),
self.second_contributor.as_str()
)
}
}
impl std::error::Error for CliBuildError {}