Skip to main content

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