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