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}