use std::{
error::Error,
fmt,
sync::{Arc, OnceLock, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard},
};
use crate::{Command, StrategyError, cli::CommandRegistry};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum LockPoisonPolicy {
FailFast = 0,
Recover = 1,
}
pub trait HelpRenderer: Send + Sync {
fn render(&self, caller: &str, registered_commands: &[Command]) -> String;
}
pub struct PlainTextHelpRenderer;
impl HelpRenderer for PlainTextHelpRenderer {
fn render(&self, caller: &str, registered_commands: &[Command]) -> String {
fn render_recursive(command: &Command, depth: usize, path: String, out: &mut Vec<String>) {
let indent = " ".repeat(depth);
out.push(format!(
" {indent}- {path}: {}",
command.metadata.description
));
if let Some(catalog) = command.subcommand_catalog() {
for child in catalog.subcommands() {
let child_path = format!("{path} {}", child.metadata.name);
render_recursive(&child, depth + 1, child_path, out);
}
}
}
let mut lines = Vec::new();
for command in registered_commands {
render_recursive(command, 0, command.metadata.name.clone(), &mut lines);
}
let command_lines = lines.join("\n");
format!(
r#"Usage: {} <command> [args...]
Registered commands are listed below.
supported commands:
- help : Display help information
{}
"#,
caller, command_lines
)
}
}
pub struct CoreConfig {
pub lock_poison_policy: LockPoisonPolicy,
pub help_renderer: Arc<dyn HelpRenderer>,
}
impl CoreConfig {
pub fn new() -> Self {
Self {
lock_poison_policy: LockPoisonPolicy::FailFast,
help_renderer: Arc::new(PlainTextHelpRenderer),
}
}
pub fn with_lock_poison_policy(mut self, policy: LockPoisonPolicy) -> Self {
self.lock_poison_policy = policy;
self
}
pub fn with_help_renderer<R>(mut self, renderer: R) -> Self
where
R: HelpRenderer + 'static,
{
self.help_renderer = Arc::new(renderer);
self
}
}
impl Default for CoreConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub enum CliCoreError {
MissingCommand { help: String },
UnknownCommand { command: String, help: String },
StrategyExecution {
command: String,
source: StrategyError,
},
}
impl fmt::Display for CliCoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingCommand { help } => {
write!(f, "No command provided.\n\n{help}")
}
Self::UnknownCommand { command, help } => {
write!(f, "Unknown command: {command}\n\n{help}")
}
Self::StrategyExecution { command, source } => {
write!(f, "Strategy execution failed for '{command}': {source}")
}
}
}
}
impl Error for CliCoreError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::StrategyExecution { source, .. } => Some(source),
_ => None,
}
}
}
pub struct CliCore {
registry: OnceLock<Arc<RwLock<CommandRegistry>>>,
config: CoreConfig,
}
impl Default for CliCore {
fn default() -> Self {
Self::new()
}
}
impl CliCore {
pub fn create(config: CoreConfig) -> Self {
Self {
registry: OnceLock::new(),
config,
}
}
pub fn new() -> Self {
Self {
registry: OnceLock::new(),
config: CoreConfig::new(),
}
}
pub fn lock_poison_policy(&self) -> LockPoisonPolicy {
self.config.lock_poison_policy
}
pub fn register(&self, command: Command) -> &Self {
let mut guard = self.write_registry();
guard.register(command);
self
}
pub fn get(&self, name: &str) -> Option<Command> {
let guard = self.read_registry();
guard.get(name)
}
pub fn get_all(&self) -> Vec<Command> {
let guard = self.read_registry();
guard.get_all()
}
pub fn run_with_commands(&self, commands: &[Command]) {
if let Err(e) = self.try_run_with_commands(commands) {
eprintln!("{e}");
}
}
pub fn try_run_with_commands(&self, commands: &[Command]) -> Result<(), CliCoreError> {
for command in commands {
self.register(command.clone());
}
self.try_run_from_env()
}
pub fn try_run_from_args(&self, args: &[String]) -> Result<(), CliCoreError> {
let binary = args.get(0).cloned().unwrap_or_else(|| "cli".to_string());
if args.get(1).is_some_and(|arg| arg == "help") {
println!("{}", self.render_help(&binary));
return Ok(());
}
if args.len() < 2 {
return Err(CliCoreError::MissingCommand {
help: self.render_help(&binary),
});
}
let command_name = args[1].clone();
let command_args = args.get(2..).unwrap_or(&[]).to_vec();
let command = self
.get(&command_name)
.ok_or_else(|| CliCoreError::UnknownCommand {
command: command_name.clone(),
help: self.render_help(&binary),
})?;
command
.execute(command_args)
.map_err(|source| CliCoreError::StrategyExecution {
command: command_name,
source,
})
}
fn render_help(&self, caller: &str) -> String {
self.config.help_renderer.render(caller, &self.get_all())
}
fn try_run_from_env(&self) -> Result<(), CliCoreError> {
let argv = std::env::args().collect::<Vec<String>>();
self.try_run_from_args(&argv)
}
fn registry(&self) -> &Arc<RwLock<CommandRegistry>> {
self.registry
.get_or_init(|| Arc::new(RwLock::new(CommandRegistry::new())))
}
fn read_registry(&self) -> RwLockReadGuard<'_, CommandRegistry> {
match self.registry().read() {
Ok(guard) => guard,
Err(poisoned) => self.handle_poison(poisoned, "read"),
}
}
fn write_registry(&self) -> RwLockWriteGuard<'_, CommandRegistry> {
match self.registry().write() {
Ok(guard) => guard,
Err(poisoned) => self.handle_poison(poisoned, "write"),
}
}
fn handle_poison<T>(&self, poisoned: PoisonError<T>, operation: &str) -> T {
match self.lock_poison_policy() {
LockPoisonPolicy::FailFast => {
panic!("cli-core registry lock poisoned during {operation} operation")
}
LockPoisonPolicy::Recover => poisoned.into_inner(),
}
}
}
pub fn run_with_commands(commands: &[Command]) {
CliCore::new().run_with_commands(commands)
}
pub fn try_run_with_commands(commands: &[Command]) -> Result<(), CliCoreError> {
CliCore::new().try_run_with_commands(commands)
}
#[cfg(test)]
mod tests;