use std::ffi::OsString;
use std::sync::Arc;
use clap::Command as ClapCommand;
use rtb_app::app::App;
use rtb_app::command::{Command as RtbCommand, BUILTIN_COMMANDS};
use rtb_app::features::Features;
use rtb_app::metadata::ToolMetadata;
use rtb_app::version::VersionInfo;
use rtb_assets::Assets;
use rtb_config::Config;
use crate::runtime::{self, LogFormat};
pub struct Application {
app: App,
commands: Vec<Box<dyn RtbCommand>>,
install_hooks: bool,
}
impl Application {
pub fn builder() -> ApplicationBuilder<NoMetadata, NoVersion> {
ApplicationBuilder::new()
}
pub async fn run(self) -> miette::Result<()> {
let args: Vec<OsString> = std::env::args_os().collect();
self.run_with_args(args).await
}
pub async fn run_with_args<I, S>(self, args: I) -> miette::Result<()>
where
I: IntoIterator<Item = S>,
S: Into<OsString> + Clone,
{
if self.install_hooks {
rtb_error::hook::install_report_handler();
rtb_error::hook::install_panic_hook();
if let Some(footer) = self.app.metadata.help.footer() {
rtb_error::hook::install_with_footer(move || footer.clone());
}
}
runtime::install_tracing(LogFormat::auto());
runtime::bind_shutdown_signals(self.app.shutdown.clone());
let clap_cmd = build_clap_tree(&self.app.metadata, &self.commands);
let matches = match clap_cmd.try_get_matches_from(args) {
Ok(m) => m,
Err(e) if is_help_or_version(&e) => {
print!("{e}");
return Ok(());
}
Err(e) => return Err(map_clap_error(&e)),
};
let Some((sub, _sub_matches)) = matches.subcommand() else {
return Err(rtb_error::Error::CommandNotFound("<none>".into()).into());
};
let cmd = self
.commands
.iter()
.find(|c| c.spec().name == sub)
.ok_or_else(|| rtb_error::Error::CommandNotFound(sub.to_string()))?;
cmd.run(self.app.clone()).await
}
}
pub struct NoMetadata;
pub struct HasMetadata(ToolMetadata);
pub struct NoVersion;
pub struct HasVersion(VersionInfo);
#[must_use]
pub struct ApplicationBuilder<M, V> {
metadata: M,
version: V,
assets: Option<Assets>,
features: Option<Features>,
install_hooks: bool,
credentials_provider: Option<Arc<dyn rtb_app::credentials::CredentialProvider>>,
typed_config: Option<rtb_app::typed_config::ErasedConfig>,
typed_config_ops: Option<Arc<rtb_app::typed_config::TypedConfigOps>>,
}
impl ApplicationBuilder<NoMetadata, NoVersion> {
pub fn new() -> Self {
Self {
metadata: NoMetadata,
version: NoVersion,
assets: None,
features: None,
install_hooks: true,
credentials_provider: None,
typed_config: None,
typed_config_ops: None,
}
}
}
impl Default for ApplicationBuilder<NoMetadata, NoVersion> {
fn default() -> Self {
Self::new()
}
}
impl<V> ApplicationBuilder<NoMetadata, V> {
pub fn metadata(self, m: ToolMetadata) -> ApplicationBuilder<HasMetadata, V> {
ApplicationBuilder {
metadata: HasMetadata(m),
version: self.version,
assets: self.assets,
features: self.features,
install_hooks: self.install_hooks,
credentials_provider: self.credentials_provider,
typed_config: self.typed_config,
typed_config_ops: self.typed_config_ops,
}
}
}
impl<M> ApplicationBuilder<M, NoVersion> {
pub fn version(self, v: VersionInfo) -> ApplicationBuilder<M, HasVersion> {
ApplicationBuilder {
metadata: self.metadata,
version: HasVersion(v),
assets: self.assets,
features: self.features,
install_hooks: self.install_hooks,
credentials_provider: self.credentials_provider,
typed_config: self.typed_config,
typed_config_ops: self.typed_config_ops,
}
}
}
impl<M, V> ApplicationBuilder<M, V> {
pub fn assets(mut self, a: Assets) -> Self {
self.assets = Some(a);
self
}
pub fn features(mut self, f: Features) -> Self {
self.features = Some(f);
self
}
pub const fn install_hooks(mut self, yes: bool) -> Self {
self.install_hooks = yes;
self
}
pub fn credentials_from<T>(mut self, provider: Arc<T>) -> Self
where
T: rtb_credentials::CredentialBearing + Send + Sync + 'static,
{
self.credentials_provider =
Some(provider as Arc<dyn rtb_app::credentials::CredentialProvider>);
self
}
pub fn config<C>(mut self, config: rtb_config::Config<C>) -> Self
where
C: serde::Serialize
+ serde::de::DeserializeOwned
+ schemars::JsonSchema
+ Send
+ Sync
+ 'static,
{
let ops = rtb_app::typed_config::TypedConfigOps::new::<C>();
self.typed_config = Some(rtb_app::typed_config::erase(config));
self.typed_config_ops = Some(Arc::new(ops));
self
}
}
impl ApplicationBuilder<HasMetadata, HasVersion> {
pub fn build(self) -> miette::Result<Application> {
let HasMetadata(metadata) = self.metadata;
let HasVersion(version) = self.version;
let features = self.features.unwrap_or_default();
let assets = self.assets.unwrap_or_default();
let app =
App::new(metadata, version, Config::<()>::default(), assets, self.credentials_provider);
let app = match (self.typed_config, self.typed_config_ops) {
(Some(erased), Some(ops)) => app.with_typed_config(erased, ops),
_ => app,
};
let mut commands: Vec<Box<dyn RtbCommand>> = Vec::new();
for factory in BUILTIN_COMMANDS {
let cmd = factory();
let enabled_here = cmd.spec().feature.is_none_or(|f| features.is_enabled(f));
if enabled_here {
commands.push(cmd);
}
}
let mut seen: std::collections::HashMap<&'static str, usize> =
std::collections::HashMap::new();
for (idx, cmd) in commands.iter().enumerate() {
seen.insert(cmd.spec().name, idx);
}
let keep: std::collections::HashSet<usize> = seen.values().copied().collect();
let mut i = 0usize;
commands.retain(|_| {
let keep_this = keep.contains(&i);
i += 1;
keep_this
});
commands.sort_by(|a, b| a.spec().name.cmp(b.spec().name));
Ok(Application { app, commands, install_hooks: self.install_hooks })
}
}
fn build_clap_tree(metadata: &ToolMetadata, commands: &[Box<dyn RtbCommand>]) -> ClapCommand {
let mut root = ClapCommand::new(metadata.name.clone())
.about(metadata.summary.clone())
.arg_required_else_help(true)
.subcommand_required(true)
.arg(
clap::Arg::new("output")
.long("output")
.global(true)
.value_parser(clap::value_parser!(crate::render::OutputMode))
.default_value("text")
.help("Output rendering mode for structured-output subcommands"),
);
if !metadata.description.is_empty() {
root = root.long_about(metadata.description.clone());
}
for cmd in commands {
let spec = cmd.spec();
let mut sub = ClapCommand::new(spec.name).about(spec.about);
for alias in spec.aliases {
sub = sub.alias(*alias);
}
if cmd.subcommand_passthrough() {
sub = sub.arg(
clap::Arg::new("rest")
.num_args(0..)
.trailing_var_arg(true)
.allow_hyphen_values(true),
);
sub = sub.disable_help_flag(true);
}
root = root.subcommand(sub);
}
root
}
fn is_help_or_version(err: &clap::Error) -> bool {
use clap::error::ErrorKind;
matches!(err.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion)
}
fn map_clap_error(err: &clap::Error) -> miette::Report {
use clap::error::ErrorKind;
match err.kind() {
ErrorKind::InvalidSubcommand | ErrorKind::UnknownArgument => {
let name = err
.get(clap::error::ContextKind::InvalidSubcommand)
.or_else(|| err.get(clap::error::ContextKind::InvalidArg))
.map_or_else(|| err.to_string(), |v| format!("{v}"));
rtb_error::Error::CommandNotFound(name).into()
}
_ => miette::miette!("{}", err),
}
}