use crate::config::schema::CommandDefinition;
use crate::error::{RegistryError, Result};
use crate::executor::{AsyncCommandHandler, CommandHandler};
use std::collections::HashMap;
enum StoredHandler {
Sync(Box<dyn CommandHandler>),
Async(Box<dyn AsyncCommandHandler>),
}
pub struct CommandRegistry {
commands: HashMap<String, (CommandDefinition, StoredHandler)>,
aliases: HashMap<String, String>,
}
impl CommandRegistry {
pub fn new() -> Self {
Self {
commands: HashMap::new(),
aliases: HashMap::new(),
}
}
fn check_name_available(&self, name: &str) -> Result<()> {
if self.commands.contains_key(name) {
return Err(RegistryError::DuplicateRegistration {
name: name.to_string(),
suggestion: None,
}
.into());
}
if let Some(existing_cmd) = self.aliases.get(name) {
return Err(RegistryError::DuplicateAlias {
alias: name.to_string(),
existing_command: existing_cmd.clone(),
suggestion: None,
}
.into());
}
Ok(())
}
fn insert_aliases(&mut self, definition: CommandDefinition) {
for alias in &definition.aliases {
self.aliases.insert(alias.clone(), definition.name.clone());
}
}
pub fn register_sync(
&mut self,
definition: CommandDefinition,
handler: Box<dyn CommandHandler>,
) -> Result<()> {
self.check_name_available(&definition.name)?;
for alias in &definition.aliases {
self.check_name_available(alias)?;
}
self.insert_aliases(definition.clone());
self.commands.insert(
definition.name.clone(),
(definition, StoredHandler::Sync(handler)),
);
Ok(())
}
#[deprecated(
since = "0.5.0",
note = "renamed to `register_sync` for symmetry with `register_async`; \
will be removed in 1.0.0"
)]
pub fn register(
&mut self,
definition: CommandDefinition,
handler: Box<dyn CommandHandler>,
) -> Result<()> {
self.register_sync(definition, handler)
}
pub fn register_async(
&mut self,
definition: CommandDefinition,
handler: Box<dyn AsyncCommandHandler>,
) -> Result<()> {
self.check_name_available(&definition.name)?;
for alias in &definition.aliases {
self.check_name_available(alias)?;
}
self.insert_aliases(definition.clone());
self.commands.insert(
definition.name.clone(),
(definition, StoredHandler::Async(handler)),
);
Ok(())
}
pub fn resolve_name(&self, name: &str) -> Option<&str> {
if let Some((cmd_def, _)) = self.commands.get(name) {
return Some(cmd_def.name.as_str());
}
self.aliases.get(name).map(|s| s.as_str())
}
pub fn get_definition(&self, name: &str) -> Option<&CommandDefinition> {
let canonical_name = self.resolve_name(name)?;
self.commands.get(canonical_name).map(|(def, _)| def)
}
pub fn get_handler_sync(&self, name: &str) -> Option<&dyn CommandHandler> {
let canonical = self.resolve_name(name)?;
match &self.commands.get(canonical)?.1 {
StoredHandler::Sync(h) => Some(h.as_ref()),
StoredHandler::Async(_) => None,
}
}
#[deprecated(
since = "0.5.0",
note = "renamed to `get_handler_sync` for symmetry with `get_handler_async`; \
will be removed in 1.0.0"
)]
pub fn get_handler(&self, name: &str) -> Option<&dyn CommandHandler> {
self.get_handler_sync(name)
}
pub fn get_handler_async(&self, name: &str) -> Option<&dyn AsyncCommandHandler> {
let canonical = self.resolve_name(name)?;
match &self.commands.get(canonical)?.1 {
StoredHandler::Async(h) => Some(h.as_ref()),
StoredHandler::Sync(_) => None,
}
}
pub fn list_commands(&self) -> Vec<&CommandDefinition> {
self.commands.values().map(|(def, _)| def).collect()
}
pub fn len(&self) -> usize {
self.commands.len()
}
pub fn is_empty(&self) -> bool {
self.commands.is_empty()
}
pub fn contains(&self, name: &str) -> bool {
self.resolve_name(name).is_some()
}
}
impl Default for CommandRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::any::Any;
#[derive(Default)]
struct TestContext;
impl crate::context::ExecutionContext for TestContext {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
struct TestHandler;
impl CommandHandler for TestHandler {
fn execute(
&self,
_context: &mut dyn crate::context::ExecutionContext,
_args: &HashMap<String, String>,
) -> crate::error::Result<()> {
Ok(())
}
}
struct TestAsyncHandler;
#[async_trait::async_trait]
impl AsyncCommandHandler for TestAsyncHandler {
async fn execute(
&self,
_context: &mut dyn crate::context::ExecutionContext,
_args: &HashMap<String, String>,
) -> crate::error::Result<()> {
Ok(())
}
}
fn create_test_definition(name: &str, aliases: Vec<&str>) -> CommandDefinition {
CommandDefinition {
name: name.to_string(),
aliases: aliases.iter().map(|s| s.to_string()).collect(),
description: format!("{} command", name),
required: false,
arguments: vec![],
options: vec![],
implementation: format!("{}_handler", name),
}
}
#[test]
fn test_new_registry_is_empty() {
let registry = CommandRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
assert_eq!(registry.list_commands().len(), 0);
}
#[test]
fn test_register_command() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("test", vec![]);
let result = registry.register_sync(definition, Box::new(TestHandler));
assert!(result.is_ok());
assert_eq!(registry.len(), 1);
assert!(!registry.is_empty());
}
#[test]
#[allow(deprecated)]
fn test_deprecated_register_alias_still_works() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("legacy", vec!["old"]);
let result = registry.register(definition, Box::new(TestHandler));
assert!(result.is_ok());
assert!(registry.get_handler("legacy").is_some());
assert!(registry.get_handler("old").is_some());
assert_eq!(registry.resolve_name("old"), Some("legacy"));
}
#[test]
fn test_register_command_with_aliases() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("hello", vec!["hi", "greet"]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
assert_eq!(registry.len(), 1);
assert!(registry.contains("hello"));
assert!(registry.contains("hi"));
assert!(registry.contains("greet"));
}
#[test]
fn test_register_duplicate_command_fails() {
let mut registry = CommandRegistry::new();
let def1 = create_test_definition("test", vec![]);
let def2 = create_test_definition("test", vec![]);
registry.register_sync(def1, Box::new(TestHandler)).unwrap();
let result = registry.register_sync(def2, Box::new(TestHandler));
assert!(result.is_err());
match result.unwrap_err() {
crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
name,
..
}) => {
assert_eq!(name, "test");
}
_ => panic!("Wrong error type"),
}
}
#[test]
fn test_register_duplicate_alias_fails() {
let mut registry = CommandRegistry::new();
let def1 = create_test_definition("cmd1", vec!["c"]);
let def2 = create_test_definition("cmd2", vec!["c"]);
registry.register_sync(def1, Box::new(TestHandler)).unwrap();
let result = registry.register_sync(def2, Box::new(TestHandler));
assert!(result.is_err());
match result.unwrap_err() {
crate::error::DynamicCliError::Registry(RegistryError::DuplicateAlias {
alias,
existing_command,
..
}) => {
assert_eq!(alias, "c");
assert_eq!(existing_command, "cmd1");
}
_ => panic!("Wrong error type"),
}
}
#[test]
fn test_alias_conflicts_with_command_name() {
let mut registry = CommandRegistry::new();
let def1 = create_test_definition("test", vec![]);
let def2 = create_test_definition("other", vec!["test"]);
registry.register_sync(def1, Box::new(TestHandler)).unwrap();
let result = registry.register_sync(def2, Box::new(TestHandler));
assert!(result.is_err());
}
#[test]
fn test_command_name_conflicts_with_alias() {
let mut registry = CommandRegistry::new();
let def1 = create_test_definition("cmd1", vec!["other"]);
let def2 = create_test_definition("other", vec![]);
registry.register_sync(def1, Box::new(TestHandler)).unwrap();
let result = registry.register_sync(def2, Box::new(TestHandler));
assert!(result.is_err());
}
#[test]
fn test_resolve_command_name() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("test", vec![]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
assert_eq!(registry.resolve_name("test"), Some("test"));
}
#[test]
fn test_resolve_alias() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("hello", vec!["hi", "greet"]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
assert_eq!(registry.resolve_name("hi"), Some("hello"));
assert_eq!(registry.resolve_name("greet"), Some("hello"));
}
#[test]
fn test_resolve_unknown_name() {
let registry = CommandRegistry::new();
assert_eq!(registry.resolve_name("unknown"), None);
}
#[test]
fn test_get_definition_by_name() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("test", vec![]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
let retrieved = registry.get_definition("test");
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().name, "test");
}
#[test]
fn test_get_definition_by_alias() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("hello", vec!["hi"]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
let retrieved = registry.get_definition("hi");
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().name, "hello");
}
#[test]
fn test_get_definition_unknown() {
let registry = CommandRegistry::new();
assert!(registry.get_definition("unknown").is_none());
}
#[test]
fn test_get_handler_by_name() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("test", vec![]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
let handler = registry.get_handler_sync("test");
assert!(handler.is_some());
}
#[test]
fn test_get_handler_sync_by_name() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("test", vec![]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
let handler = registry.get_handler_sync("test");
assert!(handler.is_some());
}
#[test]
fn test_get_handler_by_alias() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("hello", vec!["hi"]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
let handler = registry.get_handler_sync("hi");
assert!(handler.is_some());
}
#[test]
fn test_get_handler_unknown() {
let registry = CommandRegistry::new();
assert!(registry.get_handler_sync("unknown").is_none());
}
#[test]
fn test_list_commands_empty() {
let registry = CommandRegistry::new();
let commands = registry.list_commands();
assert_eq!(commands.len(), 0);
}
#[test]
fn test_list_commands_multiple() {
let mut registry = CommandRegistry::new();
registry
.register_sync(
create_test_definition("cmd1", vec![]),
Box::new(TestHandler),
)
.unwrap();
registry
.register_sync(
create_test_definition("cmd2", vec![]),
Box::new(TestHandler),
)
.unwrap();
registry
.register_sync(
create_test_definition("cmd3", vec![]),
Box::new(TestHandler),
)
.unwrap();
let commands = registry.list_commands();
assert_eq!(commands.len(), 3);
let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"cmd1"));
assert!(names.contains(&"cmd2"));
assert!(names.contains(&"cmd3"));
}
#[test]
fn test_complete_workflow() {
let mut registry = CommandRegistry::new();
let def1 = create_test_definition("simulate", vec!["sim", "run"]);
let def2 = create_test_definition("validate", vec!["val", "check"]);
let def3 = create_test_definition("help", vec!["h", "?"]);
registry.register_sync(def1, Box::new(TestHandler)).unwrap();
registry.register_sync(def2, Box::new(TestHandler)).unwrap();
registry.register_sync(def3, Box::new(TestHandler)).unwrap();
assert_eq!(registry.len(), 3);
assert_eq!(registry.resolve_name("simulate"), Some("simulate"));
assert_eq!(registry.resolve_name("sim"), Some("simulate"));
assert_eq!(registry.resolve_name("validate"), Some("validate"));
assert_eq!(registry.resolve_name("val"), Some("validate"));
assert!(registry.get_handler_sync("simulate").is_some());
assert!(registry.get_handler_sync("sim").is_some());
assert!(registry.get_handler_sync("h").is_some());
let sim_def = registry.get_definition("sim");
assert!(sim_def.is_some());
assert_eq!(sim_def.unwrap().name, "simulate");
}
#[test]
fn test_default_trait() {
let registry: CommandRegistry = Default::default();
assert!(registry.is_empty());
}
#[test]
fn test_contains_method() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("test", vec!["t"]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
assert!(registry.contains("test"));
assert!(registry.contains("t"));
assert!(!registry.contains("unknown"));
}
#[test]
fn test_multiple_aliases_same_command() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("command", vec!["c", "cmd", "com"]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
assert_eq!(registry.resolve_name("c"), Some("command"));
assert_eq!(registry.resolve_name("cmd"), Some("command"));
assert_eq!(registry.resolve_name("com"), Some("command"));
let handler1 = registry.get_handler_sync("c");
let handler2 = registry.get_handler_sync("cmd");
assert!(handler1.is_some());
assert!(handler2.is_some());
}
#[test]
fn test_case_sensitivity() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("Test", vec![]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
assert!(registry.contains("Test"));
assert!(!registry.contains("test"));
assert!(!registry.contains("TEST"));
}
#[test]
fn test_empty_alias_list() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("test", vec![]);
let result = registry.register_sync(definition, Box::new(TestHandler));
assert!(result.is_ok());
assert!(registry.contains("test"));
}
#[test]
fn test_register_async_command() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("fetch", vec![]);
let result = registry.register_async(definition, Box::new(TestAsyncHandler));
assert!(result.is_ok());
assert_eq!(registry.len(), 1);
}
#[test]
fn test_register_async_command_with_aliases() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("fetch", vec!["f", "get-remote"]);
registry
.register_async(definition, Box::new(TestAsyncHandler))
.unwrap();
assert!(registry.contains("fetch"));
assert!(registry.contains("f"));
assert!(registry.contains("get-remote"));
assert_eq!(registry.resolve_name("f"), Some("fetch"));
}
#[test]
fn test_get_handler_async_by_name_and_alias() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("fetch", vec!["f"]);
registry
.register_async(definition, Box::new(TestAsyncHandler))
.unwrap();
assert!(registry.get_handler_async("fetch").is_some());
assert!(registry.get_handler_async("f").is_some());
assert!(registry.get_handler_async("unknown").is_none());
}
#[test]
fn test_sync_accessor_returns_none_for_async_command() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("fetch", vec![]);
registry
.register_async(definition, Box::new(TestAsyncHandler))
.unwrap();
assert!(registry.get_handler_sync("fetch").is_none());
assert!(registry.get_handler_async("fetch").is_some());
}
#[test]
fn test_async_accessor_returns_none_for_sync_command() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("test", vec![]);
registry
.register_sync(definition, Box::new(TestHandler))
.unwrap();
assert!(registry.get_handler_async("test").is_none());
assert!(registry.get_handler_sync("test").is_some());
}
#[test]
fn test_register_async_conflicts_with_existing_sync_name() {
let mut registry = CommandRegistry::new();
let sync_def = create_test_definition("dual", vec![]);
let async_def = create_test_definition("dual", vec![]);
registry
.register_sync(sync_def, Box::new(TestHandler))
.unwrap();
let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
assert!(result.is_err());
match result.unwrap_err() {
crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
name,
..
}) => {
assert_eq!(name, "dual");
}
other => panic!("Expected DuplicateRegistration, got: {:?}", other),
}
}
#[test]
fn test_register_sync_conflicts_with_existing_async_name() {
let mut registry = CommandRegistry::new();
let async_def = create_test_definition("dual", vec![]);
let sync_def = create_test_definition("dual", vec![]);
registry
.register_async(async_def, Box::new(TestAsyncHandler))
.unwrap();
let result = registry.register_sync(sync_def, Box::new(TestHandler));
assert!(result.is_err());
}
#[test]
fn test_async_alias_conflicts_with_sync_alias() {
let mut registry = CommandRegistry::new();
let sync_def = create_test_definition("cmd1", vec!["shared"]);
let async_def = create_test_definition("cmd2", vec!["shared"]);
registry
.register_sync(sync_def, Box::new(TestHandler))
.unwrap();
let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
assert!(result.is_err());
}
#[test]
fn test_get_definition_works_for_async_command() {
let mut registry = CommandRegistry::new();
let definition = create_test_definition("fetch", vec!["f"]);
registry
.register_async(definition, Box::new(TestAsyncHandler))
.unwrap();
let retrieved = registry.get_definition("f");
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().name, "fetch");
}
#[test]
fn test_list_commands_includes_both_sync_and_async() {
let mut registry = CommandRegistry::new();
registry
.register_sync(
create_test_definition("sync-cmd", vec![]),
Box::new(TestHandler),
)
.unwrap();
registry
.register_async(
create_test_definition("async-cmd", vec![]),
Box::new(TestAsyncHandler),
)
.unwrap();
let commands = registry.list_commands();
assert_eq!(commands.len(), 2);
let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"sync-cmd"));
assert!(names.contains(&"async-cmd"));
}
#[test]
fn test_mixed_registry_workflow() {
let mut registry = CommandRegistry::new();
registry
.register_sync(
create_test_definition("simulate", vec!["sim"]),
Box::new(TestHandler),
)
.unwrap();
registry
.register_async(
create_test_definition("fetch", vec!["f"]),
Box::new(TestAsyncHandler),
)
.unwrap();
assert_eq!(registry.len(), 2);
assert!(registry.contains("sim"));
assert!(registry.contains("f"));
assert!(registry.get_handler_sync("simulate").is_some());
assert!(registry.get_handler_async("fetch").is_some());
assert!(registry.get_handler_sync("fetch").is_none());
assert!(registry.get_handler_async("simulate").is_none());
}
}