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