use std::{any::TypeId, collections::HashSet};
use clap::{Arg, ArgAction, Command};
use schemars::JsonSchema;
use crate::{
model::{ArgumentInfo, CliContract, DiscoveryNode, ExecutableData},
schema::{
ExtendedSchemaFactory, SchemaFactory, compose_extended_schemas, extended_schema_factory,
output_schema_factory,
},
};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("unknown clap command path: {path}", path = format_path(.path))]
UnknownCommand {
path: Vec<String>,
},
#[error("duplicate executable command registration: {path}", path = format_path(.path))]
DuplicateCommandRegistration {
path: Vec<String>,
},
#[error("application-wide extension schema may only be declared once")]
DuplicateApplicationExtension,
#[error(
"command-specific extension requires an executable command: {path}",
path = format_path(.path)
)]
CommandExtensionRequiresExecutable {
path: Vec<String>,
},
#[error("derived CommandSchema registration does not match clap subcommands for `{type_name}`")]
DerivedCommandMismatch {
type_name: &'static str,
},
#[error(
"command {path} has nested clap subcommands; derive CommandSchema for its Args payload",
path = format_path(.path)
)]
UnregisteredSubcommands {
path: Vec<String>,
},
}
#[derive(Debug, Clone)]
pub(crate) struct PendingCommandRegistration {
path: Vec<String>,
id: TypeId,
output: Option<SchemaFactory>,
extended: Option<ExtendedSchemaFactory>,
}
#[derive(Debug, Default)]
pub(crate) struct RegistrationState {
registrations: Vec<PendingCommandRegistration>,
extended: Vec<ExtendedSchemaFactory>,
}
impl RegistrationState {
pub(crate) fn command<T>(&mut self, path: Vec<String>, extended: Option<ExtendedSchemaFactory>)
where
T: crate::__private::HandlerContract,
{
self.registrations.push(PendingCommandRegistration {
path,
id: TypeId::of::<T>(),
output: output_schema_factory::<T>(),
extended,
});
}
pub(crate) fn command_extension<T>(
&mut self,
path: &[String],
extended: ExtendedSchemaFactory,
) -> Result<()>
where
T: 'static,
{
let id = TypeId::of::<T>();
let Some(registration) = self
.registrations
.iter_mut()
.rev()
.find(|registration| registration.id == id && registration.path == path)
else {
return Err(Error::CommandExtensionRequiresExecutable { path: path.to_vec() });
};
registration.extended = Some(extended);
Ok(())
}
pub(crate) fn extend(&mut self, extended: ExtendedSchemaFactory) {
self.extended.push(extended);
}
}
#[derive(Debug)]
pub struct ContractBuilder {
root: Command,
registration: RegistrationState,
}
impl ContractBuilder {
#[must_use]
pub const fn new(root: Command) -> Self {
Self {
root,
registration: RegistrationState { registrations: Vec::new(), extended: Vec::new() },
}
}
pub(crate) const fn with_registration(root: Command, registration: RegistrationState) -> Self {
Self { root, registration }
}
#[must_use]
pub fn command<T>(mut self, path: impl IntoIterator<Item = impl Into<String>>) -> Self
where
T: crate::__private::HandlerContract,
{
self.registration.command::<T>(path.into_iter().map(Into::into).collect(), None);
self
}
#[must_use]
pub fn command_with_extension<T, E>(
mut self,
path: impl IntoIterator<Item = impl Into<String>>,
) -> Self
where
T: crate::__private::HandlerContract,
E: JsonSchema,
{
self.registration.command::<T>(
path.into_iter().map(Into::into).collect(),
Some(extended_schema_factory::<E>()),
);
self
}
#[must_use]
pub fn extend<T>(mut self) -> Self
where
T: JsonSchema,
{
self.registration.extend(extended_schema_factory::<T>());
self
}
pub fn build(self) -> Result<CliContract> {
let Self { mut root, registration } = self;
let RegistrationState { registrations, extended } = registration;
let extended = unique_application_extension(&extended)?;
root.build();
reject_duplicate_paths(®istrations)?;
let application_extended_schema = extended.map(ExtendedSchemaFactory::root);
let mut registrations = registrations;
let discovery = discovery_tree(&root, &mut registrations, extended);
if let Some(registration) = registrations.first() {
return Err(Error::UnknownCommand { path: registration.path.clone() });
}
Ok(CliContract { discovery, extended_schema: application_extended_schema })
}
}
const fn unique_application_extension(
extended: &[ExtendedSchemaFactory],
) -> Result<Option<ExtendedSchemaFactory>> {
match extended {
[] => Ok(None),
[extended] => Ok(Some(*extended)),
_ => Err(Error::DuplicateApplicationExtension),
}
}
fn reject_duplicate_paths(registrations: &[PendingCommandRegistration]) -> Result<()> {
let mut seen = HashSet::with_capacity(registrations.len());
for registration in registrations {
if !seen.insert(registration.path.clone()) {
return Err(Error::DuplicateCommandRegistration { path: registration.path.clone() });
}
}
Ok(())
}
fn discovery_tree(
root: &Command,
registrations: &mut Vec<PendingCommandRegistration>,
application_extension: Option<ExtendedSchemaFactory>,
) -> DiscoveryNode {
build_discovery_node(root, Vec::new(), registrations, application_extension, false, true)
.expect("the root discovery node is always retained")
}
fn build_discovery_node(
command: &Command,
path: Vec<String>,
registrations: &mut Vec<PendingCommandRegistration>,
application_extension: Option<ExtendedSchemaFactory>,
ancestor_hidden: bool,
root: bool,
) -> Option<DiscoveryNode> {
let hidden = ancestor_hidden || command.is_hide_set();
let pending = registrations
.iter()
.position(|registration| registration.path == path)
.map(|index| registrations.remove(index));
let mut children = Vec::new();
for child in command.get_subcommands() {
let mut child_path = path.clone();
child_path.push(child.get_name().to_owned());
if let Some(child) = build_discovery_node(
child,
child_path,
registrations,
application_extension,
hidden,
false,
) {
children.push(child);
}
}
children.sort_by(|left, right| left.name.cmp(&right.name));
if !root && hidden {
return None;
}
let executable = if hidden {
None
} else {
pending.map(|registration| {
let extended_schema = registration.extended.map(|extension| {
application_extension.map_or_else(
|| extension.root(),
|application| compose_extended_schemas(application, extension),
)
});
ExecutableData {
id: registration.id,
output: registration.output.map(|factory| factory()),
extended_schema,
}
})
};
if !root && executable.is_none() && children.is_empty() {
return None;
}
Some(DiscoveryNode {
name: command.get_name().to_owned(),
path,
aliases: command.get_all_aliases().map(ToOwned::to_owned).collect(),
visible_aliases: command.get_visible_aliases().map(ToOwned::to_owned).collect(),
description: command
.get_about()
.or_else(|| command.get_long_about())
.map(ToString::to_string),
usage: usage_synopsis(command),
arguments: reflected_positionals(command),
options: reflected_options(command),
executable,
children,
})
}
fn reflected_positionals(command: &Command) -> Vec<ArgumentInfo> {
command
.get_positionals()
.filter(|argument| reflected_argument(argument))
.map(argument_info)
.collect()
}
fn reflected_options(command: &Command) -> Vec<ArgumentInfo> {
command
.get_arguments()
.filter(|argument| !argument.is_positional())
.filter(|argument| reflected_argument(argument))
.map(argument_info)
.collect()
}
fn reflected_argument(argument: &Arg) -> bool {
if argument.is_hide_set() {
return false;
}
!matches!(
argument.get_action(),
ArgAction::Help | ArgAction::HelpShort | ArgAction::HelpLong | ArgAction::Version
)
}
fn argument_info(argument: &Arg) -> ArgumentInfo {
let takes_values = argument.get_action().takes_values();
let default_values = if takes_values && !argument.is_hide_default_value_set() {
argument
.get_default_values()
.iter()
.filter_map(|value| value.to_str())
.map(ToOwned::to_owned)
.collect()
} else {
Vec::new()
};
let possible_values = if takes_values && !argument.is_hide_possible_values_set() {
argument
.get_possible_values()
.into_iter()
.filter(|value| !value.is_hide_set())
.map(|value| value.get_name().to_owned())
.collect()
} else {
Vec::new()
};
ArgumentInfo {
id: argument.get_id().to_string(),
index: argument.get_index(),
short: argument.get_short(),
long: argument.get_long().map(ToOwned::to_owned),
short_aliases: argument.get_visible_short_aliases().unwrap_or_default(),
aliases: argument
.get_visible_aliases()
.unwrap_or_default()
.into_iter()
.map(ToOwned::to_owned)
.collect(),
value_names: if takes_values {
argument.get_value_names().unwrap_or_default().iter().map(ToString::to_string).collect()
} else {
Vec::new()
},
help: argument.get_help().or_else(|| argument.get_long_help()).map(ToString::to_string),
required: argument.is_required_set(),
default_values,
possible_values,
}
}
fn usage_synopsis(command: &Command) -> String {
let mut command = command.clone();
let rendered = command.render_usage().to_string();
rendered.strip_prefix("Usage: ").unwrap_or(&rendered).trim().to_owned()
}
fn format_path(path: &[String]) -> String {
if path.is_empty() { "<root>".to_owned() } else { path.join(" ") }
}