Skip to main content

dynamic_cli/registry/
command_registry.rs

1//! Command registry implementation
2//!
3//! This module provides the central registry for storing and retrieving
4//! command definitions and their associated handlers.
5//!
6//! # Architecture
7//!
8//! The registry maintains two main data structures:
9//! - A map of command names to their definitions and handlers
10//! - A map of aliases to canonical command names
11//!
12//! This design allows O(1) lookup by both command name and alias.
13//!
14//! # Example
15//!
16//! ```
17//! use dynamic_cli::registry::CommandRegistry;
18//! use dynamic_cli::config::schema::CommandDefinition;
19//! use dynamic_cli::executor::{CommandHandler, ParsedArgs};
20//!
21//! // Create a registry
22//! let mut registry = CommandRegistry::new();
23//!
24//! // Define a command
25//! let definition = CommandDefinition {
26//!     name: "hello".to_string(),
27//!     aliases: vec!["hi".to_string(), "greet".to_string()],
28//!     description: "Say hello".to_string(),
29//!     required: false,
30//!     arguments: vec![],
31//!     options: vec![],
32//!     implementation: "hello_handler".to_string(),
33//! };
34//!
35//! // Create a handler
36//! struct HelloCommand;
37//! impl CommandHandler for HelloCommand {
38//!     fn execute(
39//!         &self,
40//!         _ctx: &mut dyn dynamic_cli::context::ExecutionContext,
41//!         _args: &ParsedArgs,
42//!     ) -> dynamic_cli::Result<()> {
43//!         println!("Hello!");
44//!         Ok(())
45//!     }
46//! }
47//!
48//! // Register the command
49//! registry.register_sync(definition, Box::new(HelloCommand))?;
50//!
51//! // Retrieve by name
52//! assert!(registry.get_handler_sync("hello").is_some());
53//!
54//! // Retrieve by alias
55//! assert_eq!(registry.resolve_name("hi"), Some("hello"));
56//! # Ok::<(), dynamic_cli::error::DynamicCliError>(())
57//! ```
58
59use crate::config::schema::CommandDefinition;
60use crate::error::{RegistryError, Result};
61use crate::executor::{AsyncCommandHandler, CommandHandler};
62use std::collections::HashMap;
63
64/// Internal storage for a single registered command's handler.
65///
66/// Private — never leaks into the public API. `get_handler_sync()` /
67/// `get_handler_async()` return `None` when queried against the wrong
68/// variant, so callers never need to know this enum exists. See DD-022 for
69/// the rationale behind unifying sync and async storage in one map instead
70/// of two parallel `HashMap`s.
71enum StoredHandler {
72    Sync(Box<dyn CommandHandler>),
73    Async(Box<dyn AsyncCommandHandler>),
74}
75/// Central registry for commands and their handlers
76///
77/// The registry stores all registered commands along with their definitions
78/// and handlers. It provides efficient lookup by both command name and alias.
79///
80/// # Thread Safety
81///
82/// The registry is designed to be constructed once during application startup
83/// and then shared immutably across the application. For multi-threaded access,
84/// wrap it in `Arc<CommandRegistry>`.
85///
86/// # Example
87///
88/// ```
89/// use dynamic_cli::registry::CommandRegistry;
90/// use dynamic_cli::config::schema::CommandDefinition;
91/// use dynamic_cli::executor::CommandHandler;
92/// use std::collections::HashMap;
93///
94/// let mut registry = CommandRegistry::new();
95///
96/// // Register commands during initialization
97/// # let definition = CommandDefinition {
98/// #     name: "test".to_string(),
99/// #     aliases: vec![],
100/// #     description: "Test".to_string(),
101/// #     required: false,
102/// #     arguments: vec![],
103/// #     options: vec![],
104/// #     implementation: "test_handler".to_string(),
105/// # };
106/// # struct TestCommand;
107/// # impl CommandHandler for TestCommand {
108/// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
109/// # }
110/// registry.register_sync(definition, Box::new(TestCommand))?;
111///
112/// // Use throughout the application
113/// if let Some(handler) = registry.get_handler_sync("test") {
114///     // Execute the command
115/// }
116/// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
117/// ```
118pub struct CommandRegistry {
119    /// Map of command names to their data
120    /// Key: canonical command name
121    /// Value: (CommandDefinition, Box<dyn CommandHandler>)
122    commands: HashMap<String, (CommandDefinition, StoredHandler)>,
123
124    /// Map of aliases to canonical command names
125    /// Key: alias
126    /// Value: canonical command name
127    ///
128    /// This allows O(1) resolution of aliases to command names.
129    aliases: HashMap<String, String>,
130}
131
132impl CommandRegistry {
133    /// Create a new empty registry
134    ///
135    /// # Example
136    ///
137    /// ```
138    /// use dynamic_cli::registry::CommandRegistry;
139    ///
140    /// let registry = CommandRegistry::new();
141    /// assert_eq!(registry.list_commands().len(), 0);
142    /// ```
143    pub fn new() -> Self {
144        Self {
145            commands: HashMap::new(),
146            aliases: HashMap::new(),
147        }
148    }
149
150    /// Checks that `name` is free to use as a command name or alias.
151    ///
152    /// Shared by [`register_sync`][Self::register_sync] and
153    /// [`register_async`][Self::register_async] — a name can never belong
154    /// to both a sync and an async handler, nor be duplicated as a command
155    /// or an alias. Checked against the single unified `commands` map, so
156    /// this one call covers both storage kinds.
157    ///
158    /// # Errors
159    ///
160    /// - [`RegistryError::DuplicateRegistration`] if `name` is already a
161    ///   registered command (sync or async).
162    /// - [`RegistryError::DuplicateAlias`] if `name` is already registered
163    ///   as an alias of another command.
164    fn check_name_available(&self, name: &str) -> Result<()> {
165        if self.commands.contains_key(name) {
166            return Err(RegistryError::DuplicateRegistration {
167                name: name.to_string(),
168                suggestion: None,
169            }
170            .into());
171        }
172
173        if let Some(existing_cmd) = self.aliases.get(name) {
174            return Err(RegistryError::DuplicateAlias {
175                alias: name.to_string(),
176                existing_command: existing_cmd.clone(),
177                suggestion: None,
178            }
179            .into());
180        }
181
182        Ok(())
183    }
184
185    /// Registers every alias declared in `definition` as pointing to
186    /// `definition.name`. Called by both `register_sync` and
187    /// `register_async` after `check_name_available` has confirmed there is
188    /// no conflict.
189    fn insert_aliases(&mut self, definition: CommandDefinition) {
190        for alias in &definition.aliases {
191            self.aliases.insert(alias.clone(), definition.name.clone());
192        }
193    }
194
195    /// Register a command with its (sync) handler
196    ///
197    /// This method registers a command definition along with its handler.
198    /// It also registers all aliases for the command.
199    ///
200    /// Renamed from `register()` in v0.5.0 for symmetry with
201    /// [`register_async`][Self::register_async]. `register()` remains
202    /// available as a deprecated alias until v1.0.0 (DD-022).
203    ///
204    /// # Arguments
205    ///
206    /// * `definition` - The command definition from the configuration
207    /// * `handler` - The handler implementation for this command
208    ///
209    /// # Returns
210    ///
211    /// - `Ok(())` if registration succeeds
212    /// - `Err(RegistryError)` if:
213    ///   - A command with the same name is already registered (sync or async)
214    ///   - An alias conflicts with an existing command or alias
215    ///
216    /// # Errors
217    ///
218    /// - [`RegistryError::DuplicateRegistration`] if the command name already exists
219    /// - [`RegistryError::DuplicateAlias`] if an alias is already in use
220    ///
221    /// # Example
222    ///
223    /// ```
224    /// use dynamic_cli::registry::CommandRegistry;
225    /// use dynamic_cli::config::schema::CommandDefinition;
226    /// use dynamic_cli::executor::{CommandHandler, ParsedArgs};
227    ///
228    /// let mut registry = CommandRegistry::new();
229    ///
230    /// let definition = CommandDefinition {
231    ///     name: "simulate".to_string(),
232    ///     aliases: vec!["sim".to_string(), "run".to_string()],
233    ///     description: "Run simulation".to_string(),
234    ///     required: false,
235    ///     arguments: vec![],
236    ///     options: vec![],
237    ///     implementation: "sim_handler".to_string(),
238    /// };
239    ///
240    /// struct SimCommand;
241    /// impl CommandHandler for SimCommand {
242    ///     fn execute(
243    ///         &self,
244    ///         _: &mut dyn dynamic_cli::context::ExecutionContext,
245    ///         _: &ParsedArgs,
246    ///     ) -> dynamic_cli::Result<()> {
247    ///         Ok(())
248    ///     }
249    /// }
250    ///
251    /// // Register the command
252    /// registry.register_sync(definition, Box::new(SimCommand))?;
253    ///
254    /// // Can now access by name or alias
255    /// assert!(registry.get_handler_sync("simulate").is_some());
256    /// assert_eq!(registry.resolve_name("sim"), Some("simulate"));
257    /// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
258    /// ```
259    pub fn register_sync(
260        &mut self,
261        definition: CommandDefinition,
262        handler: Box<dyn CommandHandler>,
263    ) -> Result<()> {
264        self.check_name_available(&definition.name)?;
265        for alias in &definition.aliases {
266            self.check_name_available(alias)?;
267        }
268        self.insert_aliases(definition.clone());
269        self.commands.insert(
270            definition.name.clone(),
271            (definition, StoredHandler::Sync(handler)),
272        );
273        Ok(())
274    }
275
276    /// Deprecated alias for [`register_sync`][Self::register_sync].
277    ///
278    /// Kept for backward compatibility with pre-0.5.0 consumers (e.g.
279    /// `chrom-rs`). Scheduled for removal in v1.0.0, batched with the other
280    /// breaking changes tracked in the v1.0.0 API cleanup issue.
281    #[deprecated(
282        since = "0.5.0",
283        note = "renamed to `register_sync` for symmetry with `register_async`; \
284                will be removed in 1.0.0"
285    )]
286    pub fn register(
287        &mut self,
288        definition: CommandDefinition,
289        handler: Box<dyn CommandHandler>,
290    ) -> Result<()> {
291        self.register_sync(definition, handler)
292    }
293
294    /// Register a command with its async handler (DD-022)
295    ///
296    /// Additive counterpart of [`register_sync`][Self::register_sync] —
297    /// same conflict-detection rules (checked against both sync and async
298    /// registrations sharing the unified internal storage, plus aliases),
299    /// same alias handling.
300    ///
301    /// # Errors
302    ///
303    /// - [`RegistryError::DuplicateRegistration`] if the command name already exists
304    /// - [`RegistryError::DuplicateAlias`] if an alias is already in use
305    ///
306    /// # Example
307    ///
308    /// ```
309    /// use dynamic_cli::registry::CommandRegistry;
310    /// use dynamic_cli::config::schema::CommandDefinition;
311    /// use dynamic_cli::executor::{AsyncCommandHandler, ParsedArgs};
312    /// use async_trait::async_trait;
313    ///
314    /// let mut registry = CommandRegistry::new();
315    ///
316    /// let definition = CommandDefinition {
317    ///     name: "fetch".to_string(),
318    ///     aliases: vec![],
319    ///     description: "Fetch remote data".to_string(),
320    ///     required: false,
321    ///     arguments: vec![],
322    ///     options: vec![],
323    ///     implementation: "fetch_handler".to_string(),
324    /// };
325    ///
326    /// struct FetchCommand;
327    /// #[async_trait]
328    /// impl AsyncCommandHandler for FetchCommand {
329    ///     async fn execute(
330    ///         &self,
331    ///         _: &mut dyn dynamic_cli::context::ExecutionContext,
332    ///         _: &ParsedArgs,
333    ///     ) -> dynamic_cli::Result<()> {
334    ///         Ok(())
335    ///     }
336    /// }
337    ///
338    /// registry.register_async(definition, Box::new(FetchCommand))?;
339    /// assert!(registry.get_handler_async("fetch").is_some());
340    /// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
341    /// ```
342    pub fn register_async(
343        &mut self,
344        definition: CommandDefinition,
345        handler: Box<dyn AsyncCommandHandler>,
346    ) -> Result<()> {
347        self.check_name_available(&definition.name)?;
348        for alias in &definition.aliases {
349            self.check_name_available(alias)?;
350        }
351        self.insert_aliases(definition.clone());
352        self.commands.insert(
353            definition.name.clone(),
354            (definition, StoredHandler::Async(handler)),
355        );
356        Ok(())
357    }
358
359    /// Resolve a name (command or alias) to the canonical command name
360    ///
361    /// This method checks if the given name is either:
362    /// - A registered command name (returns the name itself)
363    /// - An alias (returns the canonical command name)
364    ///
365    /// # Arguments
366    ///
367    /// * `name` - The name or alias to resolve
368    ///
369    /// # Returns
370    ///
371    /// - `Some(&str)` - The canonical command name
372    /// - `None` - If the name is not registered
373    ///
374    /// # Example
375    ///
376    /// ```
377    /// use dynamic_cli::registry::CommandRegistry;
378    /// # use dynamic_cli::config::schema::CommandDefinition;
379    /// # use dynamic_cli::executor::CommandHandler;
380    /// # use std::collections::HashMap;
381    ///
382    /// let mut registry = CommandRegistry::new();
383    ///
384    /// # let definition = CommandDefinition {
385    /// #     name: "hello".to_string(),
386    /// #     aliases: vec!["hi".to_string()],
387    /// #     description: "".to_string(),
388    /// #     required: false,
389    /// #     arguments: vec![],
390    /// #     options: vec![],
391    /// #     implementation: "".to_string(),
392    /// # };
393    /// # struct TestCmd;
394    /// # impl CommandHandler for TestCmd {
395    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
396    /// # }
397    /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
398    /// // Resolve command name
399    /// assert_eq!(registry.resolve_name("hello"), Some("hello"));
400    ///
401    /// // Resolve alias
402    /// assert_eq!(registry.resolve_name("hi"), Some("hello"));
403    ///
404    /// // Unknown name
405    /// assert_eq!(registry.resolve_name("unknown"), None);
406    /// ```
407    pub fn resolve_name(&self, name: &str) -> Option<&str> {
408        // First check if it's a command name
409        // Return reference to the stored name, not the parameter
410        if let Some((cmd_def, _)) = self.commands.get(name) {
411            return Some(cmd_def.name.as_str());
412        }
413
414        // Then check if it's an alias
415        self.aliases.get(name).map(|s| s.as_str())
416    }
417
418    /// Get the definition of a command by name or alias
419    ///
420    /// # Arguments
421    ///
422    /// * `name` - The command name or alias
423    ///
424    /// # Returns
425    ///
426    /// - `Some(&CommandDefinition)` if the command exists
427    /// - `None` if the command is not registered
428    ///
429    /// # Example
430    ///
431    /// ```
432    /// # use dynamic_cli::registry::CommandRegistry;
433    /// # use dynamic_cli::config::schema::CommandDefinition;
434    /// # use dynamic_cli::executor::CommandHandler;
435    /// # use std::collections::HashMap;
436    /// # let mut registry = CommandRegistry::new();
437    /// # let definition = CommandDefinition {
438    /// #     name: "test".to_string(),
439    /// #     aliases: vec!["t".to_string()],
440    /// #     description: "Test command".to_string(),
441    /// #     required: false,
442    /// #     arguments: vec![],
443    /// #     options: vec![],
444    /// #     implementation: "".to_string(),
445    /// # };
446    /// # struct TestCmd;
447    /// # impl CommandHandler for TestCmd {
448    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
449    /// # }
450    /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
451    /// // Get by name
452    /// if let Some(def) = registry.get_definition("test") {
453    ///     assert_eq!(def.name, "test");
454    ///     assert_eq!(def.description, "Test command");
455    /// }
456    ///
457    /// // Get by alias
458    /// if let Some(def) = registry.get_definition("t") {
459    ///     assert_eq!(def.name, "test");
460    /// }
461    /// ```
462    pub fn get_definition(&self, name: &str) -> Option<&CommandDefinition> {
463        let canonical_name = self.resolve_name(name)?;
464        self.commands.get(canonical_name).map(|(def, _)| def)
465    }
466
467    /// Get the (sync) handler of a command by name or alias
468    ///
469    /// This is the primary method used during CLI/REPL dispatch to
470    /// retrieve the handler that will execute the command. Returns `None`
471    /// both when the name isn't registered at all, and when it resolves to
472    /// an *async* handler (query [`get_handler_async`][Self::get_handler_async]
473    /// instead in that case) — dispatch sites try both in sequence.
474    ///
475    /// Renamed from `get_handler()` in v0.5.0 for symmetry with
476    /// [`get_handler_async`][Self::get_handler_async]. `get_handler()`
477    /// remains available as a deprecated alias until v1.0.0 (DD-022).
478    ///
479    /// # Arguments
480    ///
481    /// * `name` - The command name or alias
482    ///
483    /// # Returns
484    ///
485    /// - `Some(&dyn CommandHandler)` if a sync handler is registered under this name
486    /// - `None` if unregistered, or if registered as an async handler
487    ///
488    /// # Example
489    ///
490    /// ```
491    /// # use dynamic_cli::registry::CommandRegistry;
492    /// # use dynamic_cli::config::schema::CommandDefinition;
493    /// # use dynamic_cli::executor::CommandHandler;
494    /// # use std::collections::HashMap;
495    /// # let mut registry = CommandRegistry::new();
496    /// # let definition = CommandDefinition {
497    /// #     name: "exec".to_string(),
498    /// #     aliases: vec!["x".to_string()],
499    /// #     description: "".to_string(),
500    /// #     required: false,
501    /// #     arguments: vec![],
502    /// #     options: vec![],
503    /// #     implementation: "".to_string(),
504    /// # };
505    /// # struct ExecCmd;
506    /// # impl CommandHandler for ExecCmd {
507    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
508    /// # }
509    /// # registry.register_sync(definition, Box::new(ExecCmd)).unwrap();
510    /// // Get handler by name
511    /// if let Some(handler) = registry.get_handler_sync("exec") {
512    ///     // Use handler for execution
513    /// }
514    ///
515    /// // Get handler by alias
516    /// if let Some(handler) = registry.get_handler_sync("x") {
517    ///     // Same handler
518    /// }
519    /// ```
520    // The return type &dyn CommandHandler is intentional: callers receive a
521    // reference to the handler, which preserves the indirection needed for
522    // dynamic dispatch without transferring ownership.
523    pub fn get_handler_sync(&self, name: &str) -> Option<&dyn CommandHandler> {
524        let canonical = self.resolve_name(name)?;
525        match &self.commands.get(canonical)?.1 {
526            StoredHandler::Sync(h) => Some(h.as_ref()),
527            StoredHandler::Async(_) => None,
528        }
529    }
530
531    /// Deprecated alias for [`get_handler_sync`][Self::get_handler_sync].
532    /// Scheduled for removal in v1.0.0.
533    #[deprecated(
534        since = "0.5.0",
535        note = "renamed to `get_handler_sync` for symmetry with `get_handler_async`; \
536                will be removed in 1.0.0"
537    )]
538    pub fn get_handler(&self, name: &str) -> Option<&dyn CommandHandler> {
539        self.get_handler_sync(name)
540    }
541
542    /// Get the async handler of a command by name or alias (DD-022)
543    ///
544    /// Additive counterpart of [`get_handler_sync`][Self::get_handler_sync].
545    /// Returns `None` both when the name isn't registered at all, and when
546    /// it resolves to a *sync* handler.
547    ///
548    /// # Example
549    ///
550    /// ```
551    /// # use dynamic_cli::registry::CommandRegistry;
552    /// # use dynamic_cli::config::schema::CommandDefinition;
553    /// # use dynamic_cli::executor::AsyncCommandHandler;
554    /// # use std::collections::HashMap;
555    /// # use async_trait::async_trait;
556    /// # let mut registry = CommandRegistry::new();
557    /// # let definition = CommandDefinition {
558    /// #     name: "fetch".to_string(),
559    /// #     aliases: vec![],
560    /// #     description: "".to_string(),
561    /// #     required: false,
562    /// #     arguments: vec![],
563    /// #     options: vec![],
564    /// #     implementation: "".to_string(),
565    /// # };
566    /// # struct FetchCmd;
567    /// # #[async_trait]
568    /// # impl AsyncCommandHandler for FetchCmd {
569    /// #     async fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
570    /// # }
571    /// # registry.register_async(definition, Box::new(FetchCmd)).unwrap();
572    /// assert!(registry.get_handler_async("fetch").is_some());
573    /// assert!(registry.get_handler_sync("fetch").is_none()); // wrong accessor
574    /// ```
575    pub fn get_handler_async(&self, name: &str) -> Option<&dyn AsyncCommandHandler> {
576        let canonical = self.resolve_name(name)?;
577        match &self.commands.get(canonical)?.1 {
578            StoredHandler::Async(h) => Some(h.as_ref()),
579            StoredHandler::Sync(_) => None,
580        }
581    }
582
583    /// List all registered command definitions
584    ///
585    /// Returns a vector of references to all command definitions in the registry.
586    /// The order is not guaranteed.
587    ///
588    /// # Returns
589    ///
590    /// Vector of command definition references
591    ///
592    /// # Example
593    ///
594    /// ```
595    /// # use dynamic_cli::registry::CommandRegistry;
596    /// # use dynamic_cli::config::schema::CommandDefinition;
597    /// # use dynamic_cli::executor::CommandHandler;
598    /// # use std::collections::HashMap;
599    /// # let mut registry = CommandRegistry::new();
600    /// # let def1 = CommandDefinition {
601    /// #     name: "cmd1".to_string(),
602    /// #     aliases: vec![],
603    /// #     description: "".to_string(),
604    /// #     required: false,
605    /// #     arguments: vec![],
606    /// #     options: vec![],
607    /// #     implementation: "".to_string(),
608    /// # };
609    /// # let def2 = CommandDefinition {
610    /// #     name: "cmd2".to_string(),
611    /// #     aliases: vec![],
612    /// #     description: "".to_string(),
613    /// #     required: false,
614    /// #     arguments: vec![],
615    /// #     options: vec![],
616    /// #     implementation: "".to_string(),
617    /// # };
618    /// # struct TestCmd;
619    /// # impl CommandHandler for TestCmd {
620    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
621    /// # }
622    /// # registry.register_sync(def1, Box::new(TestCmd)).unwrap();
623    /// # registry.register_sync(def2, Box::new(TestCmd)).unwrap();
624    /// let commands = registry.list_commands();
625    /// assert_eq!(commands.len(), 2);
626    ///
627    /// // Use for help text, command completion, etc.
628    /// for cmd in commands {
629    ///     println!("{}: {}", cmd.name, cmd.description);
630    /// }
631    /// ```
632    pub fn list_commands(&self) -> Vec<&CommandDefinition> {
633        self.commands.values().map(|(def, _)| def).collect()
634    }
635
636    /// Get the number of registered commands
637    ///
638    /// # Example
639    ///
640    /// ```
641    /// use dynamic_cli::registry::CommandRegistry;
642    ///
643    /// let registry = CommandRegistry::new();
644    /// assert_eq!(registry.len(), 0);
645    /// ```
646    pub fn len(&self) -> usize {
647        self.commands.len()
648    }
649
650    /// Check if the registry is empty
651    ///
652    /// # Example
653    ///
654    /// ```
655    /// use dynamic_cli::registry::CommandRegistry;
656    ///
657    /// let registry = CommandRegistry::new();
658    /// assert!(registry.is_empty());
659    /// ```
660    pub fn is_empty(&self) -> bool {
661        self.commands.is_empty()
662    }
663
664    /// Check if a command is registered (by name or alias)
665    ///
666    /// # Example
667    ///
668    /// ```
669    /// # use dynamic_cli::registry::CommandRegistry;
670    /// # use dynamic_cli::config::schema::CommandDefinition;
671    /// # use dynamic_cli::executor::CommandHandler;
672    /// # use std::collections::HashMap;
673    /// # let mut registry = CommandRegistry::new();
674    /// # let definition = CommandDefinition {
675    /// #     name: "test".to_string(),
676    /// #     aliases: vec!["t".to_string()],
677    /// #     description: "".to_string(),
678    /// #     required: false,
679    /// #     arguments: vec![],
680    /// #     options: vec![],
681    /// #     implementation: "".to_string(),
682    /// # };
683    /// # struct TestCmd;
684    /// # impl CommandHandler for TestCmd {
685    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
686    /// # }
687    /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
688    /// assert!(registry.contains("test"));
689    /// assert!(registry.contains("t"));
690    /// assert!(!registry.contains("unknown"));
691    /// ```
692    pub fn contains(&self, name: &str) -> bool {
693        self.resolve_name(name).is_some()
694    }
695}
696
697// Implement Default for convenience
698impl Default for CommandRegistry {
699    fn default() -> Self {
700        Self::new()
701    }
702}
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707    use crate::parser::ParsedArgs;
708    use std::any::Any;
709
710    // Test fixtures
711    #[derive(Default)]
712    struct TestContext;
713
714    impl crate::context::ExecutionContext for TestContext {
715        fn as_any(&self) -> &dyn Any {
716            self
717        }
718        fn as_any_mut(&mut self) -> &mut dyn Any {
719            self
720        }
721    }
722
723    struct TestHandler;
724
725    impl CommandHandler for TestHandler {
726        fn execute(
727            &self,
728            _context: &mut dyn crate::context::ExecutionContext,
729            _args: &ParsedArgs,
730        ) -> crate::error::Result<()> {
731            Ok(())
732        }
733    }
734
735    struct TestAsyncHandler;
736
737    #[async_trait::async_trait]
738    impl AsyncCommandHandler for TestAsyncHandler {
739        async fn execute(
740            &self,
741            _context: &mut dyn crate::context::ExecutionContext,
742            _args: &ParsedArgs,
743        ) -> crate::error::Result<()> {
744            Ok(())
745        }
746    }
747
748    fn create_test_definition(name: &str, aliases: Vec<&str>) -> CommandDefinition {
749        CommandDefinition {
750            name: name.to_string(),
751            aliases: aliases.iter().map(|s| s.to_string()).collect(),
752            description: format!("{} command", name),
753            required: false,
754            arguments: vec![],
755            options: vec![],
756            implementation: format!("{}_handler", name),
757        }
758    }
759
760    // Basic functionality tests
761    #[test]
762    fn test_new_registry_is_empty() {
763        let registry = CommandRegistry::new();
764        assert!(registry.is_empty());
765        assert_eq!(registry.len(), 0);
766        assert_eq!(registry.list_commands().len(), 0);
767    }
768
769    #[test]
770    fn test_register_command() {
771        let mut registry = CommandRegistry::new();
772        let definition = create_test_definition("test", vec![]);
773
774        let result = registry.register_sync(definition, Box::new(TestHandler));
775
776        assert!(result.is_ok());
777        assert_eq!(registry.len(), 1);
778        assert!(!registry.is_empty());
779    }
780
781    /// Deprecated-alias coverage (DD-022 companion issue): `register()` and
782    /// `get_handler()` must keep behaving exactly like `register_sync()` /
783    /// `get_handler_sync()` until they're removed in v1.0.0. This is the
784    /// only place in the crate allowed to call them directly.
785    #[test]
786    #[allow(deprecated)]
787    fn test_deprecated_register_alias_still_works() {
788        let mut registry = CommandRegistry::new();
789        let definition = create_test_definition("legacy", vec!["old"]);
790
791        let result = registry.register(definition, Box::new(TestHandler));
792
793        assert!(result.is_ok());
794        assert!(registry.get_handler("legacy").is_some());
795        assert!(registry.get_handler("old").is_some());
796        assert_eq!(registry.resolve_name("old"), Some("legacy"));
797    }
798
799    #[test]
800    fn test_register_command_with_aliases() {
801        let mut registry = CommandRegistry::new();
802        let definition = create_test_definition("hello", vec!["hi", "greet"]);
803
804        registry
805            .register_sync(definition, Box::new(TestHandler))
806            .unwrap();
807
808        assert_eq!(registry.len(), 1);
809        assert!(registry.contains("hello"));
810        assert!(registry.contains("hi"));
811        assert!(registry.contains("greet"));
812    }
813
814    #[test]
815    fn test_register_duplicate_command_fails() {
816        let mut registry = CommandRegistry::new();
817        let def1 = create_test_definition("test", vec![]);
818        let def2 = create_test_definition("test", vec![]);
819
820        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
821        let result = registry.register_sync(def2, Box::new(TestHandler));
822
823        assert!(result.is_err());
824        match result.unwrap_err() {
825            crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
826                name,
827                ..
828            }) => {
829                assert_eq!(name, "test");
830            }
831            _ => panic!("Wrong error type"),
832        }
833    }
834
835    #[test]
836    fn test_register_duplicate_alias_fails() {
837        let mut registry = CommandRegistry::new();
838        let def1 = create_test_definition("cmd1", vec!["c"]);
839        let def2 = create_test_definition("cmd2", vec!["c"]);
840
841        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
842        let result = registry.register_sync(def2, Box::new(TestHandler));
843
844        assert!(result.is_err());
845        match result.unwrap_err() {
846            crate::error::DynamicCliError::Registry(RegistryError::DuplicateAlias {
847                alias,
848                existing_command,
849                ..
850            }) => {
851                assert_eq!(alias, "c");
852                assert_eq!(existing_command, "cmd1");
853            }
854            _ => panic!("Wrong error type"),
855        }
856    }
857
858    #[test]
859    fn test_alias_conflicts_with_command_name() {
860        let mut registry = CommandRegistry::new();
861        let def1 = create_test_definition("test", vec![]);
862        let def2 = create_test_definition("other", vec!["test"]);
863
864        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
865        let result = registry.register_sync(def2, Box::new(TestHandler));
866
867        assert!(result.is_err());
868    }
869
870    #[test]
871    fn test_command_name_conflicts_with_alias() {
872        let mut registry = CommandRegistry::new();
873        let def1 = create_test_definition("cmd1", vec!["other"]);
874        let def2 = create_test_definition("other", vec![]);
875
876        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
877        let result = registry.register_sync(def2, Box::new(TestHandler));
878
879        assert!(result.is_err());
880    }
881
882    // Resolve name tests
883    #[test]
884    fn test_resolve_command_name() {
885        let mut registry = CommandRegistry::new();
886        let definition = create_test_definition("test", vec![]);
887
888        registry
889            .register_sync(definition, Box::new(TestHandler))
890            .unwrap();
891
892        assert_eq!(registry.resolve_name("test"), Some("test"));
893    }
894
895    #[test]
896    fn test_resolve_alias() {
897        let mut registry = CommandRegistry::new();
898        let definition = create_test_definition("hello", vec!["hi", "greet"]);
899
900        registry
901            .register_sync(definition, Box::new(TestHandler))
902            .unwrap();
903
904        assert_eq!(registry.resolve_name("hi"), Some("hello"));
905        assert_eq!(registry.resolve_name("greet"), Some("hello"));
906    }
907
908    #[test]
909    fn test_resolve_unknown_name() {
910        let registry = CommandRegistry::new();
911        assert_eq!(registry.resolve_name("unknown"), None);
912    }
913
914    // Get definition tests
915    #[test]
916    fn test_get_definition_by_name() {
917        let mut registry = CommandRegistry::new();
918        let definition = create_test_definition("test", vec![]);
919
920        registry
921            .register_sync(definition, Box::new(TestHandler))
922            .unwrap();
923
924        let retrieved = registry.get_definition("test");
925        assert!(retrieved.is_some());
926        assert_eq!(retrieved.unwrap().name, "test");
927    }
928
929    #[test]
930    fn test_get_definition_by_alias() {
931        let mut registry = CommandRegistry::new();
932        let definition = create_test_definition("hello", vec!["hi"]);
933
934        registry
935            .register_sync(definition, Box::new(TestHandler))
936            .unwrap();
937
938        let retrieved = registry.get_definition("hi");
939        assert!(retrieved.is_some());
940        assert_eq!(retrieved.unwrap().name, "hello");
941    }
942
943    #[test]
944    fn test_get_definition_unknown() {
945        let registry = CommandRegistry::new();
946        assert!(registry.get_definition("unknown").is_none());
947    }
948
949    // Get handler tests
950    #[test]
951    fn test_get_handler_by_name() {
952        let mut registry = CommandRegistry::new();
953        let definition = create_test_definition("test", vec![]);
954
955        registry
956            .register_sync(definition, Box::new(TestHandler))
957            .unwrap();
958
959        let handler = registry.get_handler_sync("test");
960        assert!(handler.is_some());
961    }
962
963    #[test]
964    fn test_get_handler_sync_by_name() {
965        let mut registry = CommandRegistry::new();
966        let definition = create_test_definition("test", vec![]);
967
968        registry
969            .register_sync(definition, Box::new(TestHandler))
970            .unwrap();
971
972        let handler = registry.get_handler_sync("test");
973        assert!(handler.is_some());
974    }
975
976    #[test]
977    fn test_get_handler_by_alias() {
978        let mut registry = CommandRegistry::new();
979        let definition = create_test_definition("hello", vec!["hi"]);
980
981        registry
982            .register_sync(definition, Box::new(TestHandler))
983            .unwrap();
984
985        let handler = registry.get_handler_sync("hi");
986        assert!(handler.is_some());
987    }
988
989    #[test]
990    fn test_get_handler_unknown() {
991        let registry = CommandRegistry::new();
992        assert!(registry.get_handler_sync("unknown").is_none());
993    }
994
995    // List commands tests
996    #[test]
997    fn test_list_commands_empty() {
998        let registry = CommandRegistry::new();
999        let commands = registry.list_commands();
1000        assert_eq!(commands.len(), 0);
1001    }
1002
1003    #[test]
1004    fn test_list_commands_multiple() {
1005        let mut registry = CommandRegistry::new();
1006
1007        registry
1008            .register_sync(
1009                create_test_definition("cmd1", vec![]),
1010                Box::new(TestHandler),
1011            )
1012            .unwrap();
1013        registry
1014            .register_sync(
1015                create_test_definition("cmd2", vec![]),
1016                Box::new(TestHandler),
1017            )
1018            .unwrap();
1019        registry
1020            .register_sync(
1021                create_test_definition("cmd3", vec![]),
1022                Box::new(TestHandler),
1023            )
1024            .unwrap();
1025
1026        let commands = registry.list_commands();
1027        assert_eq!(commands.len(), 3);
1028
1029        let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1030        assert!(names.contains(&"cmd1"));
1031        assert!(names.contains(&"cmd2"));
1032        assert!(names.contains(&"cmd3"));
1033    }
1034
1035    // Integration tests
1036    #[test]
1037    fn test_complete_workflow() {
1038        let mut registry = CommandRegistry::new();
1039
1040        // Register multiple commands with aliases
1041        let def1 = create_test_definition("simulate", vec!["sim", "run"]);
1042        let def2 = create_test_definition("validate", vec!["val", "check"]);
1043        let def3 = create_test_definition("help", vec!["h", "?"]);
1044
1045        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
1046        registry.register_sync(def2, Box::new(TestHandler)).unwrap();
1047        registry.register_sync(def3, Box::new(TestHandler)).unwrap();
1048
1049        // Verify registry state
1050        assert_eq!(registry.len(), 3);
1051
1052        // Verify all names resolve correctly
1053        assert_eq!(registry.resolve_name("simulate"), Some("simulate"));
1054        assert_eq!(registry.resolve_name("sim"), Some("simulate"));
1055        assert_eq!(registry.resolve_name("validate"), Some("validate"));
1056        assert_eq!(registry.resolve_name("val"), Some("validate"));
1057
1058        // Verify handlers are accessible
1059        assert!(registry.get_handler_sync("simulate").is_some());
1060        assert!(registry.get_handler_sync("sim").is_some());
1061        assert!(registry.get_handler_sync("h").is_some());
1062
1063        // Verify definitions are accessible
1064        let sim_def = registry.get_definition("sim");
1065        assert!(sim_def.is_some());
1066        assert_eq!(sim_def.unwrap().name, "simulate");
1067    }
1068
1069    #[test]
1070    fn test_default_trait() {
1071        let registry: CommandRegistry = Default::default();
1072        assert!(registry.is_empty());
1073    }
1074
1075    #[test]
1076    fn test_contains_method() {
1077        let mut registry = CommandRegistry::new();
1078        let definition = create_test_definition("test", vec!["t"]);
1079
1080        registry
1081            .register_sync(definition, Box::new(TestHandler))
1082            .unwrap();
1083
1084        assert!(registry.contains("test"));
1085        assert!(registry.contains("t"));
1086        assert!(!registry.contains("unknown"));
1087    }
1088
1089    #[test]
1090    fn test_multiple_aliases_same_command() {
1091        let mut registry = CommandRegistry::new();
1092        let definition = create_test_definition("command", vec!["c", "cmd", "com"]);
1093
1094        registry
1095            .register_sync(definition, Box::new(TestHandler))
1096            .unwrap();
1097
1098        // All aliases should resolve to the same command
1099        assert_eq!(registry.resolve_name("c"), Some("command"));
1100        assert_eq!(registry.resolve_name("cmd"), Some("command"));
1101        assert_eq!(registry.resolve_name("com"), Some("command"));
1102
1103        // All should return the same handler
1104        let handler1 = registry.get_handler_sync("c");
1105        let handler2 = registry.get_handler_sync("cmd");
1106        assert!(handler1.is_some());
1107        assert!(handler2.is_some());
1108    }
1109
1110    #[test]
1111    fn test_case_sensitivity() {
1112        let mut registry = CommandRegistry::new();
1113        let definition = create_test_definition("Test", vec![]);
1114
1115        registry
1116            .register_sync(definition, Box::new(TestHandler))
1117            .unwrap();
1118
1119        // Case matters
1120        assert!(registry.contains("Test"));
1121        assert!(!registry.contains("test"));
1122        assert!(!registry.contains("TEST"));
1123    }
1124
1125    #[test]
1126    fn test_empty_alias_list() {
1127        let mut registry = CommandRegistry::new();
1128        let definition = create_test_definition("test", vec![]);
1129
1130        let result = registry.register_sync(definition, Box::new(TestHandler));
1131
1132        assert!(result.is_ok());
1133        assert!(registry.contains("test"));
1134    }
1135
1136    // ============================================================================
1137    // AsyncCommandHandler / register_async / get_handler_async TESTS (DD-022)
1138    // ============================================================================
1139
1140    #[test]
1141    fn test_register_async_command() {
1142        let mut registry = CommandRegistry::new();
1143        let definition = create_test_definition("fetch", vec![]);
1144
1145        let result = registry.register_async(definition, Box::new(TestAsyncHandler));
1146
1147        assert!(result.is_ok());
1148        assert_eq!(registry.len(), 1);
1149    }
1150
1151    #[test]
1152    fn test_register_async_command_with_aliases() {
1153        let mut registry = CommandRegistry::new();
1154        let definition = create_test_definition("fetch", vec!["f", "get-remote"]);
1155
1156        registry
1157            .register_async(definition, Box::new(TestAsyncHandler))
1158            .unwrap();
1159
1160        assert!(registry.contains("fetch"));
1161        assert!(registry.contains("f"));
1162        assert!(registry.contains("get-remote"));
1163        assert_eq!(registry.resolve_name("f"), Some("fetch"));
1164    }
1165
1166    #[test]
1167    fn test_get_handler_async_by_name_and_alias() {
1168        let mut registry = CommandRegistry::new();
1169        let definition = create_test_definition("fetch", vec!["f"]);
1170
1171        registry
1172            .register_async(definition, Box::new(TestAsyncHandler))
1173            .unwrap();
1174
1175        assert!(registry.get_handler_async("fetch").is_some());
1176        assert!(registry.get_handler_async("f").is_some());
1177        assert!(registry.get_handler_async("unknown").is_none());
1178    }
1179
1180    /// The core cross-accessor guarantee DD-022 depends on: querying an
1181    /// async-registered command through the *sync* accessor returns `None`
1182    /// (not the wrong handler, not a panic) — dispatch sites rely on this
1183    /// to fall through from `get_handler_sync` to `get_handler_async`.
1184    #[test]
1185    fn test_sync_accessor_returns_none_for_async_command() {
1186        let mut registry = CommandRegistry::new();
1187        let definition = create_test_definition("fetch", vec![]);
1188
1189        registry
1190            .register_async(definition, Box::new(TestAsyncHandler))
1191            .unwrap();
1192
1193        assert!(registry.get_handler_sync("fetch").is_none());
1194        assert!(registry.get_handler_async("fetch").is_some());
1195    }
1196
1197    /// Symmetric case: querying a sync-registered command through the
1198    /// *async* accessor returns `None`.
1199    #[test]
1200    fn test_async_accessor_returns_none_for_sync_command() {
1201        let mut registry = CommandRegistry::new();
1202        let definition = create_test_definition("test", vec![]);
1203
1204        registry
1205            .register_sync(definition, Box::new(TestHandler))
1206            .unwrap();
1207
1208        assert!(registry.get_handler_async("test").is_none());
1209        assert!(registry.get_handler_sync("test").is_some());
1210    }
1211
1212    /// A command name already taken by a sync handler must be rejected for
1213    /// async registration — the unified storage means one name, one kind.
1214    #[test]
1215    fn test_register_async_conflicts_with_existing_sync_name() {
1216        let mut registry = CommandRegistry::new();
1217        let sync_def = create_test_definition("dual", vec![]);
1218        let async_def = create_test_definition("dual", vec![]);
1219
1220        registry
1221            .register_sync(sync_def, Box::new(TestHandler))
1222            .unwrap();
1223        let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1224
1225        assert!(result.is_err());
1226        match result.unwrap_err() {
1227            crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
1228                name,
1229                ..
1230            }) => {
1231                assert_eq!(name, "dual");
1232            }
1233            other => panic!("Expected DuplicateRegistration, got: {:?}", other),
1234        }
1235    }
1236
1237    /// Symmetric case: a name already taken by an async handler must be
1238    /// rejected for sync registration.
1239    #[test]
1240    fn test_register_sync_conflicts_with_existing_async_name() {
1241        let mut registry = CommandRegistry::new();
1242        let async_def = create_test_definition("dual", vec![]);
1243        let sync_def = create_test_definition("dual", vec![]);
1244
1245        registry
1246            .register_async(async_def, Box::new(TestAsyncHandler))
1247            .unwrap();
1248        let result = registry.register_sync(sync_def, Box::new(TestHandler));
1249
1250        assert!(result.is_err());
1251    }
1252
1253    /// An async command's alias must not collide with an existing sync
1254    /// command's alias, and vice versa — conflict detection is shared
1255    /// across both kinds via `check_name_available`.
1256    #[test]
1257    fn test_async_alias_conflicts_with_sync_alias() {
1258        let mut registry = CommandRegistry::new();
1259        let sync_def = create_test_definition("cmd1", vec!["shared"]);
1260        let async_def = create_test_definition("cmd2", vec!["shared"]);
1261
1262        registry
1263            .register_sync(sync_def, Box::new(TestHandler))
1264            .unwrap();
1265        let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1266
1267        assert!(result.is_err());
1268    }
1269
1270    #[test]
1271    fn test_get_definition_works_for_async_command() {
1272        let mut registry = CommandRegistry::new();
1273        let definition = create_test_definition("fetch", vec!["f"]);
1274
1275        registry
1276            .register_async(definition, Box::new(TestAsyncHandler))
1277            .unwrap();
1278
1279        let retrieved = registry.get_definition("f");
1280        assert!(retrieved.is_some());
1281        assert_eq!(retrieved.unwrap().name, "fetch");
1282    }
1283
1284    #[test]
1285    fn test_list_commands_includes_both_sync_and_async() {
1286        let mut registry = CommandRegistry::new();
1287
1288        registry
1289            .register_sync(
1290                create_test_definition("sync-cmd", vec![]),
1291                Box::new(TestHandler),
1292            )
1293            .unwrap();
1294        registry
1295            .register_async(
1296                create_test_definition("async-cmd", vec![]),
1297                Box::new(TestAsyncHandler),
1298            )
1299            .unwrap();
1300
1301        let commands = registry.list_commands();
1302        assert_eq!(commands.len(), 2);
1303        let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1304        assert!(names.contains(&"sync-cmd"));
1305        assert!(names.contains(&"async-cmd"));
1306    }
1307
1308    #[test]
1309    fn test_mixed_registry_workflow() {
1310        // End-to-end: a registry with both sync and async commands behaves
1311        // consistently across resolve_name / get_definition / len / contains.
1312        let mut registry = CommandRegistry::new();
1313
1314        registry
1315            .register_sync(
1316                create_test_definition("simulate", vec!["sim"]),
1317                Box::new(TestHandler),
1318            )
1319            .unwrap();
1320        registry
1321            .register_async(
1322                create_test_definition("fetch", vec!["f"]),
1323                Box::new(TestAsyncHandler),
1324            )
1325            .unwrap();
1326
1327        assert_eq!(registry.len(), 2);
1328        assert!(registry.contains("sim"));
1329        assert!(registry.contains("f"));
1330
1331        assert!(registry.get_handler_sync("simulate").is_some());
1332        assert!(registry.get_handler_async("fetch").is_some());
1333        assert!(registry.get_handler_sync("fetch").is_none());
1334        assert!(registry.get_handler_async("simulate").is_none());
1335    }
1336}