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}