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
709    // Test fixtures
710
711    struct TestHandler;
712
713    impl CommandHandler for TestHandler {
714        fn execute(
715            &self,
716            _context: &mut dyn crate::context::ExecutionContext,
717            _args: &ParsedArgs,
718        ) -> crate::error::Result<()> {
719            Ok(())
720        }
721    }
722
723    struct TestAsyncHandler;
724
725    #[async_trait::async_trait]
726    impl AsyncCommandHandler for TestAsyncHandler {
727        async fn execute(
728            &self,
729            _context: &mut dyn crate::context::ExecutionContext,
730            _args: &ParsedArgs,
731        ) -> crate::error::Result<()> {
732            Ok(())
733        }
734    }
735
736    fn create_test_definition(name: &str, aliases: Vec<&str>) -> CommandDefinition {
737        CommandDefinition {
738            name: name.to_string(),
739            aliases: aliases.iter().map(|s| s.to_string()).collect(),
740            description: format!("{} command", name),
741            required: false,
742            arguments: vec![],
743            options: vec![],
744            implementation: format!("{}_handler", name),
745        }
746    }
747
748    // Basic functionality tests
749    #[test]
750    fn test_new_registry_is_empty() {
751        let registry = CommandRegistry::new();
752        assert!(registry.is_empty());
753        assert_eq!(registry.len(), 0);
754        assert_eq!(registry.list_commands().len(), 0);
755    }
756
757    #[test]
758    fn test_register_command() {
759        let mut registry = CommandRegistry::new();
760        let definition = create_test_definition("test", vec![]);
761
762        let result = registry.register_sync(definition, Box::new(TestHandler));
763
764        assert!(result.is_ok());
765        assert_eq!(registry.len(), 1);
766        assert!(!registry.is_empty());
767    }
768
769    /// Deprecated-alias coverage (DD-022 companion issue): `register()` and
770    /// `get_handler()` must keep behaving exactly like `register_sync()` /
771    /// `get_handler_sync()` until they're removed in v1.0.0. This is the
772    /// only place in the crate allowed to call them directly.
773    #[test]
774    #[allow(deprecated)]
775    fn test_deprecated_register_alias_still_works() {
776        let mut registry = CommandRegistry::new();
777        let definition = create_test_definition("legacy", vec!["old"]);
778
779        let result = registry.register(definition, Box::new(TestHandler));
780
781        assert!(result.is_ok());
782        assert!(registry.get_handler("legacy").is_some());
783        assert!(registry.get_handler("old").is_some());
784        assert_eq!(registry.resolve_name("old"), Some("legacy"));
785    }
786
787    #[test]
788    fn test_register_command_with_aliases() {
789        let mut registry = CommandRegistry::new();
790        let definition = create_test_definition("hello", vec!["hi", "greet"]);
791
792        registry
793            .register_sync(definition, Box::new(TestHandler))
794            .unwrap();
795
796        assert_eq!(registry.len(), 1);
797        assert!(registry.contains("hello"));
798        assert!(registry.contains("hi"));
799        assert!(registry.contains("greet"));
800    }
801
802    #[test]
803    fn test_register_duplicate_command_fails() {
804        let mut registry = CommandRegistry::new();
805        let def1 = create_test_definition("test", vec![]);
806        let def2 = create_test_definition("test", vec![]);
807
808        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
809        let result = registry.register_sync(def2, Box::new(TestHandler));
810
811        assert!(result.is_err());
812        match result.unwrap_err() {
813            crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
814                name,
815                ..
816            }) => {
817                assert_eq!(name, "test");
818            }
819            _ => panic!("Wrong error type"),
820        }
821    }
822
823    #[test]
824    fn test_register_duplicate_alias_fails() {
825        let mut registry = CommandRegistry::new();
826        let def1 = create_test_definition("cmd1", vec!["c"]);
827        let def2 = create_test_definition("cmd2", vec!["c"]);
828
829        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
830        let result = registry.register_sync(def2, Box::new(TestHandler));
831
832        assert!(result.is_err());
833        match result.unwrap_err() {
834            crate::error::DynamicCliError::Registry(RegistryError::DuplicateAlias {
835                alias,
836                existing_command,
837                ..
838            }) => {
839                assert_eq!(alias, "c");
840                assert_eq!(existing_command, "cmd1");
841            }
842            _ => panic!("Wrong error type"),
843        }
844    }
845
846    #[test]
847    fn test_alias_conflicts_with_command_name() {
848        let mut registry = CommandRegistry::new();
849        let def1 = create_test_definition("test", vec![]);
850        let def2 = create_test_definition("other", vec!["test"]);
851
852        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
853        let result = registry.register_sync(def2, Box::new(TestHandler));
854
855        assert!(result.is_err());
856    }
857
858    #[test]
859    fn test_command_name_conflicts_with_alias() {
860        let mut registry = CommandRegistry::new();
861        let def1 = create_test_definition("cmd1", vec!["other"]);
862        let def2 = create_test_definition("other", vec![]);
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    // Resolve name tests
871    #[test]
872    fn test_resolve_command_name() {
873        let mut registry = CommandRegistry::new();
874        let definition = create_test_definition("test", vec![]);
875
876        registry
877            .register_sync(definition, Box::new(TestHandler))
878            .unwrap();
879
880        assert_eq!(registry.resolve_name("test"), Some("test"));
881    }
882
883    #[test]
884    fn test_resolve_alias() {
885        let mut registry = CommandRegistry::new();
886        let definition = create_test_definition("hello", vec!["hi", "greet"]);
887
888        registry
889            .register_sync(definition, Box::new(TestHandler))
890            .unwrap();
891
892        assert_eq!(registry.resolve_name("hi"), Some("hello"));
893        assert_eq!(registry.resolve_name("greet"), Some("hello"));
894    }
895
896    #[test]
897    fn test_resolve_unknown_name() {
898        let registry = CommandRegistry::new();
899        assert_eq!(registry.resolve_name("unknown"), None);
900    }
901
902    // Get definition tests
903    #[test]
904    fn test_get_definition_by_name() {
905        let mut registry = CommandRegistry::new();
906        let definition = create_test_definition("test", vec![]);
907
908        registry
909            .register_sync(definition, Box::new(TestHandler))
910            .unwrap();
911
912        let retrieved = registry.get_definition("test");
913        assert!(retrieved.is_some());
914        assert_eq!(retrieved.unwrap().name, "test");
915    }
916
917    #[test]
918    fn test_get_definition_by_alias() {
919        let mut registry = CommandRegistry::new();
920        let definition = create_test_definition("hello", vec!["hi"]);
921
922        registry
923            .register_sync(definition, Box::new(TestHandler))
924            .unwrap();
925
926        let retrieved = registry.get_definition("hi");
927        assert!(retrieved.is_some());
928        assert_eq!(retrieved.unwrap().name, "hello");
929    }
930
931    #[test]
932    fn test_get_definition_unknown() {
933        let registry = CommandRegistry::new();
934        assert!(registry.get_definition("unknown").is_none());
935    }
936
937    // Get handler tests
938    #[test]
939    fn test_get_handler_by_name() {
940        let mut registry = CommandRegistry::new();
941        let definition = create_test_definition("test", vec![]);
942
943        registry
944            .register_sync(definition, Box::new(TestHandler))
945            .unwrap();
946
947        let handler = registry.get_handler_sync("test");
948        assert!(handler.is_some());
949    }
950
951    #[test]
952    fn test_get_handler_sync_by_name() {
953        let mut registry = CommandRegistry::new();
954        let definition = create_test_definition("test", vec![]);
955
956        registry
957            .register_sync(definition, Box::new(TestHandler))
958            .unwrap();
959
960        let handler = registry.get_handler_sync("test");
961        assert!(handler.is_some());
962    }
963
964    #[test]
965    fn test_get_handler_by_alias() {
966        let mut registry = CommandRegistry::new();
967        let definition = create_test_definition("hello", vec!["hi"]);
968
969        registry
970            .register_sync(definition, Box::new(TestHandler))
971            .unwrap();
972
973        let handler = registry.get_handler_sync("hi");
974        assert!(handler.is_some());
975    }
976
977    #[test]
978    fn test_get_handler_unknown() {
979        let registry = CommandRegistry::new();
980        assert!(registry.get_handler_sync("unknown").is_none());
981    }
982
983    // List commands tests
984    #[test]
985    fn test_list_commands_empty() {
986        let registry = CommandRegistry::new();
987        let commands = registry.list_commands();
988        assert_eq!(commands.len(), 0);
989    }
990
991    #[test]
992    fn test_list_commands_multiple() {
993        let mut registry = CommandRegistry::new();
994
995        registry
996            .register_sync(
997                create_test_definition("cmd1", vec![]),
998                Box::new(TestHandler),
999            )
1000            .unwrap();
1001        registry
1002            .register_sync(
1003                create_test_definition("cmd2", vec![]),
1004                Box::new(TestHandler),
1005            )
1006            .unwrap();
1007        registry
1008            .register_sync(
1009                create_test_definition("cmd3", vec![]),
1010                Box::new(TestHandler),
1011            )
1012            .unwrap();
1013
1014        let commands = registry.list_commands();
1015        assert_eq!(commands.len(), 3);
1016
1017        let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1018        assert!(names.contains(&"cmd1"));
1019        assert!(names.contains(&"cmd2"));
1020        assert!(names.contains(&"cmd3"));
1021    }
1022
1023    // Integration tests
1024    #[test]
1025    fn test_complete_workflow() {
1026        let mut registry = CommandRegistry::new();
1027
1028        // Register multiple commands with aliases
1029        let def1 = create_test_definition("simulate", vec!["sim", "run"]);
1030        let def2 = create_test_definition("validate", vec!["val", "check"]);
1031        let def3 = create_test_definition("help", vec!["h", "?"]);
1032
1033        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
1034        registry.register_sync(def2, Box::new(TestHandler)).unwrap();
1035        registry.register_sync(def3, Box::new(TestHandler)).unwrap();
1036
1037        // Verify registry state
1038        assert_eq!(registry.len(), 3);
1039
1040        // Verify all names resolve correctly
1041        assert_eq!(registry.resolve_name("simulate"), Some("simulate"));
1042        assert_eq!(registry.resolve_name("sim"), Some("simulate"));
1043        assert_eq!(registry.resolve_name("validate"), Some("validate"));
1044        assert_eq!(registry.resolve_name("val"), Some("validate"));
1045
1046        // Verify handlers are accessible
1047        assert!(registry.get_handler_sync("simulate").is_some());
1048        assert!(registry.get_handler_sync("sim").is_some());
1049        assert!(registry.get_handler_sync("h").is_some());
1050
1051        // Verify definitions are accessible
1052        let sim_def = registry.get_definition("sim");
1053        assert!(sim_def.is_some());
1054        assert_eq!(sim_def.unwrap().name, "simulate");
1055    }
1056
1057    #[test]
1058    fn test_default_trait() {
1059        let registry: CommandRegistry = Default::default();
1060        assert!(registry.is_empty());
1061    }
1062
1063    #[test]
1064    fn test_contains_method() {
1065        let mut registry = CommandRegistry::new();
1066        let definition = create_test_definition("test", vec!["t"]);
1067
1068        registry
1069            .register_sync(definition, Box::new(TestHandler))
1070            .unwrap();
1071
1072        assert!(registry.contains("test"));
1073        assert!(registry.contains("t"));
1074        assert!(!registry.contains("unknown"));
1075    }
1076
1077    #[test]
1078    fn test_multiple_aliases_same_command() {
1079        let mut registry = CommandRegistry::new();
1080        let definition = create_test_definition("command", vec!["c", "cmd", "com"]);
1081
1082        registry
1083            .register_sync(definition, Box::new(TestHandler))
1084            .unwrap();
1085
1086        // All aliases should resolve to the same command
1087        assert_eq!(registry.resolve_name("c"), Some("command"));
1088        assert_eq!(registry.resolve_name("cmd"), Some("command"));
1089        assert_eq!(registry.resolve_name("com"), Some("command"));
1090
1091        // All should return the same handler
1092        let handler1 = registry.get_handler_sync("c");
1093        let handler2 = registry.get_handler_sync("cmd");
1094        assert!(handler1.is_some());
1095        assert!(handler2.is_some());
1096    }
1097
1098    #[test]
1099    fn test_case_sensitivity() {
1100        let mut registry = CommandRegistry::new();
1101        let definition = create_test_definition("Test", vec![]);
1102
1103        registry
1104            .register_sync(definition, Box::new(TestHandler))
1105            .unwrap();
1106
1107        // Case matters
1108        assert!(registry.contains("Test"));
1109        assert!(!registry.contains("test"));
1110        assert!(!registry.contains("TEST"));
1111    }
1112
1113    #[test]
1114    fn test_empty_alias_list() {
1115        let mut registry = CommandRegistry::new();
1116        let definition = create_test_definition("test", vec![]);
1117
1118        let result = registry.register_sync(definition, Box::new(TestHandler));
1119
1120        assert!(result.is_ok());
1121        assert!(registry.contains("test"));
1122    }
1123
1124    // ============================================================================
1125    // AsyncCommandHandler / register_async / get_handler_async TESTS (DD-022)
1126    // ============================================================================
1127
1128    #[test]
1129    fn test_register_async_command() {
1130        let mut registry = CommandRegistry::new();
1131        let definition = create_test_definition("fetch", vec![]);
1132
1133        let result = registry.register_async(definition, Box::new(TestAsyncHandler));
1134
1135        assert!(result.is_ok());
1136        assert_eq!(registry.len(), 1);
1137    }
1138
1139    #[test]
1140    fn test_register_async_command_with_aliases() {
1141        let mut registry = CommandRegistry::new();
1142        let definition = create_test_definition("fetch", vec!["f", "get-remote"]);
1143
1144        registry
1145            .register_async(definition, Box::new(TestAsyncHandler))
1146            .unwrap();
1147
1148        assert!(registry.contains("fetch"));
1149        assert!(registry.contains("f"));
1150        assert!(registry.contains("get-remote"));
1151        assert_eq!(registry.resolve_name("f"), Some("fetch"));
1152    }
1153
1154    #[test]
1155    fn test_get_handler_async_by_name_and_alias() {
1156        let mut registry = CommandRegistry::new();
1157        let definition = create_test_definition("fetch", vec!["f"]);
1158
1159        registry
1160            .register_async(definition, Box::new(TestAsyncHandler))
1161            .unwrap();
1162
1163        assert!(registry.get_handler_async("fetch").is_some());
1164        assert!(registry.get_handler_async("f").is_some());
1165        assert!(registry.get_handler_async("unknown").is_none());
1166    }
1167
1168    /// The core cross-accessor guarantee DD-022 depends on: querying an
1169    /// async-registered command through the *sync* accessor returns `None`
1170    /// (not the wrong handler, not a panic) — dispatch sites rely on this
1171    /// to fall through from `get_handler_sync` to `get_handler_async`.
1172    #[test]
1173    fn test_sync_accessor_returns_none_for_async_command() {
1174        let mut registry = CommandRegistry::new();
1175        let definition = create_test_definition("fetch", vec![]);
1176
1177        registry
1178            .register_async(definition, Box::new(TestAsyncHandler))
1179            .unwrap();
1180
1181        assert!(registry.get_handler_sync("fetch").is_none());
1182        assert!(registry.get_handler_async("fetch").is_some());
1183    }
1184
1185    /// Symmetric case: querying a sync-registered command through the
1186    /// *async* accessor returns `None`.
1187    #[test]
1188    fn test_async_accessor_returns_none_for_sync_command() {
1189        let mut registry = CommandRegistry::new();
1190        let definition = create_test_definition("test", vec![]);
1191
1192        registry
1193            .register_sync(definition, Box::new(TestHandler))
1194            .unwrap();
1195
1196        assert!(registry.get_handler_async("test").is_none());
1197        assert!(registry.get_handler_sync("test").is_some());
1198    }
1199
1200    /// A command name already taken by a sync handler must be rejected for
1201    /// async registration — the unified storage means one name, one kind.
1202    #[test]
1203    fn test_register_async_conflicts_with_existing_sync_name() {
1204        let mut registry = CommandRegistry::new();
1205        let sync_def = create_test_definition("dual", vec![]);
1206        let async_def = create_test_definition("dual", vec![]);
1207
1208        registry
1209            .register_sync(sync_def, Box::new(TestHandler))
1210            .unwrap();
1211        let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1212
1213        assert!(result.is_err());
1214        match result.unwrap_err() {
1215            crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
1216                name,
1217                ..
1218            }) => {
1219                assert_eq!(name, "dual");
1220            }
1221            other => panic!("Expected DuplicateRegistration, got: {:?}", other),
1222        }
1223    }
1224
1225    /// Symmetric case: a name already taken by an async handler must be
1226    /// rejected for sync registration.
1227    #[test]
1228    fn test_register_sync_conflicts_with_existing_async_name() {
1229        let mut registry = CommandRegistry::new();
1230        let async_def = create_test_definition("dual", vec![]);
1231        let sync_def = create_test_definition("dual", vec![]);
1232
1233        registry
1234            .register_async(async_def, Box::new(TestAsyncHandler))
1235            .unwrap();
1236        let result = registry.register_sync(sync_def, Box::new(TestHandler));
1237
1238        assert!(result.is_err());
1239    }
1240
1241    /// An async command's alias must not collide with an existing sync
1242    /// command's alias, and vice versa — conflict detection is shared
1243    /// across both kinds via `check_name_available`.
1244    #[test]
1245    fn test_async_alias_conflicts_with_sync_alias() {
1246        let mut registry = CommandRegistry::new();
1247        let sync_def = create_test_definition("cmd1", vec!["shared"]);
1248        let async_def = create_test_definition("cmd2", vec!["shared"]);
1249
1250        registry
1251            .register_sync(sync_def, Box::new(TestHandler))
1252            .unwrap();
1253        let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1254
1255        assert!(result.is_err());
1256    }
1257
1258    #[test]
1259    fn test_get_definition_works_for_async_command() {
1260        let mut registry = CommandRegistry::new();
1261        let definition = create_test_definition("fetch", vec!["f"]);
1262
1263        registry
1264            .register_async(definition, Box::new(TestAsyncHandler))
1265            .unwrap();
1266
1267        let retrieved = registry.get_definition("f");
1268        assert!(retrieved.is_some());
1269        assert_eq!(retrieved.unwrap().name, "fetch");
1270    }
1271
1272    #[test]
1273    fn test_list_commands_includes_both_sync_and_async() {
1274        let mut registry = CommandRegistry::new();
1275
1276        registry
1277            .register_sync(
1278                create_test_definition("sync-cmd", vec![]),
1279                Box::new(TestHandler),
1280            )
1281            .unwrap();
1282        registry
1283            .register_async(
1284                create_test_definition("async-cmd", vec![]),
1285                Box::new(TestAsyncHandler),
1286            )
1287            .unwrap();
1288
1289        let commands = registry.list_commands();
1290        assert_eq!(commands.len(), 2);
1291        let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1292        assert!(names.contains(&"sync-cmd"));
1293        assert!(names.contains(&"async-cmd"));
1294    }
1295
1296    #[test]
1297    fn test_mixed_registry_workflow() {
1298        // End-to-end: a registry with both sync and async commands behaves
1299        // consistently across resolve_name / get_definition / len / contains.
1300        let mut registry = CommandRegistry::new();
1301
1302        registry
1303            .register_sync(
1304                create_test_definition("simulate", vec!["sim"]),
1305                Box::new(TestHandler),
1306            )
1307            .unwrap();
1308        registry
1309            .register_async(
1310                create_test_definition("fetch", vec!["f"]),
1311                Box::new(TestAsyncHandler),
1312            )
1313            .unwrap();
1314
1315        assert_eq!(registry.len(), 2);
1316        assert!(registry.contains("sim"));
1317        assert!(registry.contains("f"));
1318
1319        assert!(registry.get_handler_sync("simulate").is_some());
1320        assert!(registry.get_handler_async("fetch").is_some());
1321        assert!(registry.get_handler_sync("fetch").is_none());
1322        assert!(registry.get_handler_async("simulate").is_none());
1323    }
1324}