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