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}