dynamic_cli/registry/
mod.rs

1//! Command registry module
2//!
3//! This module provides the central registry for storing and managing commands.
4//! The registry maintains the mapping between command names/aliases and their
5//! definitions and handlers.
6//!
7//! # Architecture
8//!
9//! The registry serves as a lookup table for the executor:
10//!
11//! ```text
12//! Configuration → Registry → Executor
13//!     (YAML)        (Store)    (Execute)
14//! ```
15//!
16//! ## Flow
17//!
18//! 1. **Initialization**: Commands are registered during application startup
19//! 2. **Lookup**: During execution, the executor queries the registry
20//! 3. **Dispatch**: The registry returns the appropriate handler
21//!
22//! # Design Principles
23//!
24//! ## Separation of Concerns
25//!
26//! The registry separates:
27//! - **Definition** (from config module): What the command accepts
28//! - **Implementation** (from executor module): What the command does
29//! - **Lookup** (this module): How to find commands
30//!
31//! ## Efficient Lookup
32//!
33//! The registry uses HashMaps for O(1) lookup by:
34//! - Command name
35//! - Command alias
36//!
37//! This ensures fast command resolution even with many registered commands.
38//!
39//! ## Validation
40//!
41//! The registry validates during registration:
42//! - No duplicate command names
43//! - No duplicate aliases
44//! - No conflicts between names and aliases
45//!
46//! # Quick Start
47//!
48//! ```
49//! use dynamic_cli::registry::CommandRegistry;
50//! use dynamic_cli::config::schema::CommandDefinition;
51//! use dynamic_cli::executor::CommandHandler;
52//! use std::collections::HashMap;
53//!
54//! // 1. Create a registry
55//! let mut registry = CommandRegistry::new();
56//!
57//! // 2. Define a command
58//! let definition = CommandDefinition {
59//!     name: "hello".to_string(),
60//!     aliases: vec!["hi".to_string()],
61//!     description: "Say hello".to_string(),
62//!     required: false,
63//!     arguments: vec![],
64//!     options: vec![],
65//!     implementation: "hello_handler".to_string(),
66//! };
67//!
68//! // 3. Create a handler
69//! struct HelloCommand;
70//! impl CommandHandler for HelloCommand {
71//!     fn execute(
72//!         &self,
73//!         _ctx: &mut dyn dynamic_cli::context::ExecutionContext,
74//!         args: &HashMap<String, String>,
75//!     ) -> dynamic_cli::Result<()> {
76//!         let default_world = "World".to_string();
77//!         let name = args.get("name").unwrap_or(&default_world);
78//!         println!("Hello, {}!", name);
79//!         Ok(())
80//!     }
81//! }
82//!
83//! // 4. Register the command
84//! registry.register(definition, Box::new(HelloCommand))?;
85//!
86//! // 5. Use the registry
87//! if let Some(handler) = registry.get_handler("hello") {
88//!     // Execute the command
89//! }
90//!
91//! // Works with aliases too!
92//! if let Some(handler) = registry.get_handler("hi") {
93//!     // Same handler
94//! }
95//! # Ok::<(), dynamic_cli::error::DynamicCliError>(())
96//! ```
97//!
98//! # Examples
99//!
100//! ## Basic Registration
101//!
102//! ```
103//! use dynamic_cli::registry::CommandRegistry;
104//! # use dynamic_cli::config::schema::CommandDefinition;
105//! # use dynamic_cli::executor::CommandHandler;
106//! # use std::collections::HashMap;
107//!
108//! let mut registry = CommandRegistry::new();
109//!
110//! # let definition = CommandDefinition {
111//! #     name: "test".to_string(),
112//! #     aliases: vec![],
113//! #     description: "".to_string(),
114//! #     required: false,
115//! #     arguments: vec![],
116//! #     options: vec![],
117//! #     implementation: "".to_string(),
118//! # };
119//! # struct TestCmd;
120//! # impl CommandHandler for TestCmd {
121//! #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
122//! # }
123//! registry.register(definition, Box::new(TestCmd))?;
124//! # Ok::<(), dynamic_cli::error::DynamicCliError>(())
125//! ```
126//!
127//! ## Command with Multiple Aliases
128//!
129//! ```
130//! # use dynamic_cli::registry::CommandRegistry;
131//! # use dynamic_cli::config::schema::CommandDefinition;
132//! # use dynamic_cli::executor::CommandHandler;
133//! # use std::collections::HashMap;
134//! # let mut registry = CommandRegistry::new();
135//! let definition = CommandDefinition {
136//!     name: "simulate".to_string(),
137//!     aliases: vec![
138//!         "sim".to_string(),
139//!         "run".to_string(),
140//!         "exec".to_string(),
141//!     ],
142//!     description: "Run a simulation".to_string(),
143//!     required: false,
144//!     arguments: vec![],
145//!     options: vec![],
146//!     implementation: "simulate_handler".to_string(),
147//! };
148//!
149//! # struct SimCmd;
150//! # impl CommandHandler for SimCmd {
151//! #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
152//! # }
153//! registry.register(definition, Box::new(SimCmd))?;
154//!
155//! // All these work:
156//! assert!(registry.contains("simulate"));
157//! assert!(registry.contains("sim"));
158//! assert!(registry.contains("run"));
159//! assert!(registry.contains("exec"));
160//! # Ok::<(), dynamic_cli::error::DynamicCliError>(())
161//! ```
162//!
163//! ## Listing All Commands
164//!
165//! ```
166//! # use dynamic_cli::registry::CommandRegistry;
167//! # use dynamic_cli::config::schema::CommandDefinition;
168//! # use dynamic_cli::executor::CommandHandler;
169//! # use std::collections::HashMap;
170//! # let mut registry = CommandRegistry::new();
171//! # let def1 = CommandDefinition {
172//! #     name: "cmd1".to_string(),
173//! #     aliases: vec![],
174//! #     description: "First command".to_string(),
175//! #     required: false,
176//! #     arguments: vec![],
177//! #     options: vec![],
178//! #     implementation: "".to_string(),
179//! # };
180//! # let def2 = CommandDefinition {
181//! #     name: "cmd2".to_string(),
182//! #     aliases: vec![],
183//! #     description: "Second command".to_string(),
184//! #     required: false,
185//! #     arguments: vec![],
186//! #     options: vec![],
187//! #     implementation: "".to_string(),
188//! # };
189//! # struct TestCmd;
190//! # impl CommandHandler for TestCmd {
191//! #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
192//! # }
193//! # registry.register(def1, Box::new(TestCmd)).unwrap();
194//! # registry.register(def2, Box::new(TestCmd)).unwrap();
195//! // Get all commands for help text
196//! for cmd in registry.list_commands() {
197//!     println!("{}: {}", cmd.name, cmd.description);
198//! }
199//! ```
200//!
201//! ## Error Handling
202//!
203//! ```
204//! # use dynamic_cli::registry::CommandRegistry;
205//! # use dynamic_cli::config::schema::CommandDefinition;
206//! # use dynamic_cli::executor::CommandHandler;
207//! # use dynamic_cli::error::{DynamicCliError, RegistryError};
208//! # use std::collections::HashMap;
209//! # let mut registry = CommandRegistry::new();
210//! # let def1 = CommandDefinition {
211//! #     name: "test".to_string(),
212//! #     aliases: vec![],
213//! #     description: "".to_string(),
214//! #     required: false,
215//! #     arguments: vec![],
216//! #     options: vec![],
217//! #     implementation: "".to_string(),
218//! # };
219//! # let def2 = CommandDefinition {
220//! #     name: "test".to_string(),
221//! #     aliases: vec![],
222//! #     description: "".to_string(),
223//! #     required: false,
224//! #     arguments: vec![],
225//! #     options: vec![],
226//! #     implementation: "".to_string(),
227//! # };
228//! # struct TestCmd;
229//! # impl CommandHandler for TestCmd {
230//! #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
231//! # }
232//! # registry.register(def1, Box::new(TestCmd)).unwrap();
233//! // Try to register duplicate
234//! let result = registry.register(def2, Box::new(TestCmd));
235//!
236//! match result {
237//!     Err(DynamicCliError::Registry(RegistryError::DuplicateRegistration { name })) => {
238//!         eprintln!("Command '{}' already registered", name);
239//!     }
240//!     _ => {}
241//! }
242//! ```
243//!
244//! # Integration with Other Modules
245//!
246//! ## With Config Module
247//!
248//! The registry stores `CommandDefinition` from the config module:
249//!
250//! ```ignore
251//! use dynamic_cli::config::loader::load_config;
252//! use dynamic_cli::registry::CommandRegistry;
253//!
254//! let config = load_config("commands.yaml")?;
255//! let mut registry = CommandRegistry::new();
256//!
257//! for cmd_def in config.commands {
258//!     let handler = create_handler(&cmd_def.implementation);
259//!     registry.register(cmd_def, handler)?;
260//! }
261//! ```
262//!
263//! ## With Executor Module
264//!
265//! The executor queries the registry to find handlers:
266//!
267//! ```ignore
268//! use dynamic_cli::registry::CommandRegistry;
269//!
270//! fn execute_command(
271//!     registry: &CommandRegistry,
272//!     command_name: &str,
273//!     context: &mut dyn ExecutionContext,
274//!     args: &HashMap<String, String>,
275//! ) -> Result<()> {
276//!     let handler = registry.get_handler(command_name)
277//!         .ok_or_else(|| anyhow::anyhow!("Unknown command"))?;
278//!     
279//!     handler.execute(context, args)
280//! }
281//! ```
282//!
283//! # Thread Safety
284//!
285//! The registry is designed for setup-once, use-many pattern:
286//!
287//! ```ignore
288//! use std::sync::Arc;
289//!
290//! // Setup phase (single-threaded)
291//! let mut registry = CommandRegistry::new();
292//! // ... register commands ...
293//!
294//! // Usage phase (can be multi-threaded)
295//! let registry = Arc::new(registry);
296//! let registry_clone = registry.clone();
297//!
298//! std::thread::spawn(move || {
299//!     // Safe to use in multiple threads
300//!     if let Some(handler) = registry_clone.get_handler("test") {
301//!         // ...
302//!     }
303//! });
304//! ```
305
306// Public submodule
307pub mod command_registry;
308
309// Public re-exports for convenience
310pub use command_registry::CommandRegistry;
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use crate::config::schema::CommandDefinition;
316    use crate::context::ExecutionContext;
317    use crate::executor::CommandHandler;
318    use std::any::Any;
319    use std::collections::HashMap;
320
321    // Test fixtures
322    #[derive(Default)]
323    struct TestContext;
324
325    impl ExecutionContext for TestContext {
326        fn as_any(&self) -> &dyn Any {
327            self
328        }
329        fn as_any_mut(&mut self) -> &mut dyn Any {
330            self
331        }
332    }
333
334    struct TestHandler;
335
336    impl CommandHandler for TestHandler {
337        fn execute(
338            &self,
339            _context: &mut dyn ExecutionContext,
340            _args: &HashMap<String, String>,
341        ) -> crate::error::Result<()> {
342            Ok(())
343        }
344    }
345
346    #[test]
347    fn test_module_reexports() {
348        // Verify that CommandRegistry is accessible from module root
349        let _registry = CommandRegistry::new();
350    }
351
352    #[test]
353    fn test_complete_workflow_integration() {
354        // Test a complete workflow using the public API
355        let mut registry = CommandRegistry::new();
356
357        // Create multiple commands
358        let simulate_def = CommandDefinition {
359            name: "simulate".to_string(),
360            aliases: vec!["sim".to_string(), "run".to_string()],
361            description: "Run simulation".to_string(),
362            required: true,
363            arguments: vec![],
364            options: vec![],
365            implementation: "sim_handler".to_string(),
366        };
367
368        let validate_def = CommandDefinition {
369            name: "validate".to_string(),
370            aliases: vec!["val".to_string()],
371            description: "Validate input".to_string(),
372            required: false,
373            arguments: vec![],
374            options: vec![],
375            implementation: "val_handler".to_string(),
376        };
377
378        // Register commands
379        registry
380            .register(simulate_def, Box::new(TestHandler))
381            .unwrap();
382        registry
383            .register(validate_def, Box::new(TestHandler))
384            .unwrap();
385
386        // Verify complete workflow
387        assert_eq!(registry.len(), 2);
388
389        // Resolve by name
390        assert_eq!(registry.resolve_name("simulate"), Some("simulate"));
391        assert_eq!(registry.resolve_name("validate"), Some("validate"));
392
393        // Resolve by alias
394        assert_eq!(registry.resolve_name("sim"), Some("simulate"));
395        assert_eq!(registry.resolve_name("val"), Some("validate"));
396
397        // Get handlers
398        assert!(registry.get_handler("simulate").is_some());
399        assert!(registry.get_handler("sim").is_some());
400        assert!(registry.get_handler("val").is_some());
401
402        // Get definitions
403        let sim_def = registry.get_definition("sim");
404        assert!(sim_def.is_some());
405        assert_eq!(sim_def.unwrap().name, "simulate");
406        assert!(sim_def.unwrap().required);
407
408        // List all commands
409        let commands = registry.list_commands();
410        assert_eq!(commands.len(), 2);
411    }
412
413    #[test]
414    fn test_use_case_command_executor_pattern() {
415        // Simulate how an executor would use the registry
416        let mut registry = CommandRegistry::new();
417
418        let def = CommandDefinition {
419            name: "test".to_string(),
420            aliases: vec!["t".to_string()],
421            description: "Test command".to_string(),
422            required: false,
423            arguments: vec![],
424            options: vec![],
425            implementation: "test_handler".to_string(),
426        };
427
428        registry.register(def, Box::new(TestHandler)).unwrap();
429
430        // Executor pattern: resolve name, then get handler
431        let user_input = "t"; // User types alias
432
433        if let Some(canonical_name) = registry.resolve_name(user_input) {
434            if let Some(handler) = registry.get_handler(canonical_name) {
435                let mut context = TestContext;
436                let args = HashMap::new();
437
438                // Execute would happen here
439                let result = handler.execute(&mut context, &args);
440                assert!(result.is_ok());
441            }
442        }
443    }
444
445    #[test]
446    fn test_use_case_help_text_generation() {
447        // Simulate generating help text from registry
448        let mut registry = CommandRegistry::new();
449
450        let def1 = CommandDefinition {
451            name: "help".to_string(),
452            aliases: vec!["h".to_string(), "?".to_string()],
453            description: "Show help information".to_string(),
454            required: false,
455            arguments: vec![],
456            options: vec![],
457            implementation: "help_handler".to_string(),
458        };
459
460        let def2 = CommandDefinition {
461            name: "exit".to_string(),
462            aliases: vec!["quit".to_string(), "q".to_string()],
463            description: "Exit the application".to_string(),
464            required: false,
465            arguments: vec![],
466            options: vec![],
467            implementation: "exit_handler".to_string(),
468        };
469
470        registry.register(def1, Box::new(TestHandler)).unwrap();
471        registry.register(def2, Box::new(TestHandler)).unwrap();
472
473        // Generate help text
474        let mut help_text = String::from("Available commands:\n");
475        for cmd in registry.list_commands() {
476            help_text.push_str(&format!("  {} - {}", cmd.name, cmd.description));
477            if !cmd.aliases.is_empty() {
478                help_text.push_str(&format!(" (aliases: {})", cmd.aliases.join(", ")));
479            }
480            help_text.push('\n');
481        }
482
483        // Verify help text contains expected information
484        assert!(help_text.contains("help"));
485        assert!(help_text.contains("exit"));
486        assert!(help_text.contains("Show help information"));
487        assert!(help_text.contains("Exit the application"));
488    }
489
490    #[test]
491    fn test_use_case_command_autocomplete() {
492        // Simulate command autocomplete functionality
493        let mut registry = CommandRegistry::new();
494
495        registry
496            .register(
497                CommandDefinition {
498                    name: "simulate".to_string(),
499                    aliases: vec![],
500                    description: "".to_string(),
501                    required: false,
502                    arguments: vec![],
503                    options: vec![],
504                    implementation: "".to_string(),
505                },
506                Box::new(TestHandler),
507            )
508            .unwrap();
509
510        registry
511            .register(
512                CommandDefinition {
513                    name: "simulation".to_string(),
514                    aliases: vec![],
515                    description: "".to_string(),
516                    required: false,
517                    arguments: vec![],
518                    options: vec![],
519                    implementation: "".to_string(),
520                },
521                Box::new(TestHandler),
522            )
523            .unwrap();
524
525        // User types "sim" - find all commands starting with "sim"
526        let prefix = "sim";
527        let matches: Vec<&str> = registry
528            .list_commands()
529            .iter()
530            .filter(|cmd| cmd.name.starts_with(prefix))
531            .map(|cmd| cmd.name.as_str())
532            .collect();
533
534        assert_eq!(matches.len(), 2);
535        assert!(matches.contains(&"simulate"));
536        assert!(matches.contains(&"simulation"));
537    }
538
539    #[test]
540    fn test_error_handling_duplicate_detection() {
541        let mut registry = CommandRegistry::new();
542
543        let def = CommandDefinition {
544            name: "test".to_string(),
545            aliases: vec![],
546            description: "".to_string(),
547            required: false,
548            arguments: vec![],
549            options: vec![],
550            implementation: "".to_string(),
551        };
552
553        // First registration succeeds
554        assert!(registry
555            .register(def.clone(), Box::new(TestHandler))
556            .is_ok());
557
558        // Second registration fails
559        let result = registry.register(def, Box::new(TestHandler));
560        assert!(result.is_err());
561
562        // Error type is correct
563        match result {
564            Err(crate::error::DynamicCliError::Registry(
565                crate::error::RegistryError::DuplicateRegistration { name },
566            )) => {
567                assert_eq!(name, "test");
568            }
569            _ => panic!("Wrong error type"),
570        }
571    }
572}