dynamic_cli/registry/command_registry.rs
1//! Command registry implementation
2//!
3//! This module provides the central registry for storing and retrieving
4//! command definitions and their associated handlers.
5//!
6//! # Architecture
7//!
8//! The registry maintains two main data structures:
9//! - A map of command names to their definitions and handlers
10//! - A map of aliases to canonical command names
11//!
12//! This design allows O(1) lookup by both command name and alias.
13//!
14//! # Example
15//!
16//! ```
17//! use dynamic_cli::registry::CommandRegistry;
18//! use dynamic_cli::config::schema::CommandDefinition;
19//! use dynamic_cli::executor::{CommandHandler, ParsedArgs};
20//!
21//! // Create a registry
22//! let mut registry = CommandRegistry::new();
23//!
24//! // Define a command
25//! let definition = CommandDefinition {
26//! name: "hello".to_string(),
27//! aliases: vec!["hi".to_string(), "greet".to_string()],
28//! description: "Say hello".to_string(),
29//! required: false,
30//! arguments: vec![],
31//! options: vec![],
32//! implementation: "hello_handler".to_string(),
33//! continue_on_failure: false,
34//! requires_success: false,
35//! };
36//!
37//! // Create a handler
38//! struct HelloCommand;
39//! impl CommandHandler for HelloCommand {
40//! fn execute(
41//! &self,
42//! _ctx: &mut dyn dynamic_cli::context::ExecutionContext,
43//! _args: &ParsedArgs,
44//! ) -> dynamic_cli::Result<()> {
45//! println!("Hello!");
46//! Ok(())
47//! }
48//! }
49//!
50//! // Register the command
51//! registry.register_sync(definition, Box::new(HelloCommand))?;
52//!
53//! // Retrieve by name
54//! assert!(registry.get_handler_sync("hello").is_some());
55//!
56//! // Retrieve by alias
57//! assert_eq!(registry.resolve_name("hi"), Some("hello"));
58//! # Ok::<(), dynamic_cli::error::DynamicCliError>(())
59//! ```
60
61use crate::config::schema::CommandDefinition;
62use crate::error::{RegistryError, Result};
63use crate::executor::{AsyncCommandHandler, CommandHandler};
64use std::collections::HashMap;
65
66/// Internal storage for a single registered command's handler.
67///
68/// Private — never leaks into the public API. `get_handler_sync()` /
69/// `get_handler_async()` return `None` when queried against the wrong
70/// variant, so callers never need to know this enum exists. See DD-022 for
71/// the rationale behind unifying sync and async storage in one map instead
72/// of two parallel `HashMap`s.
73enum StoredHandler {
74 Sync(Box<dyn CommandHandler>),
75 Async(Box<dyn AsyncCommandHandler>),
76}
77/// Central registry for commands and their handlers
78///
79/// The registry stores all registered commands along with their definitions
80/// and handlers. It provides efficient lookup by both command name and alias.
81///
82/// # Thread Safety
83///
84/// The registry is designed to be constructed once during application startup
85/// and then shared immutably across the application. For multi-threaded access,
86/// wrap it in `Arc<CommandRegistry>`.
87///
88/// # Example
89///
90/// ```
91/// use dynamic_cli::registry::CommandRegistry;
92/// use dynamic_cli::config::schema::CommandDefinition;
93/// use dynamic_cli::executor::CommandHandler;
94/// use std::collections::HashMap;
95///
96/// let mut registry = CommandRegistry::new();
97///
98/// // Register commands during initialization
99/// # let definition = CommandDefinition {
100/// # name: "test".to_string(),
101/// # aliases: vec![],
102/// # description: "Test".to_string(),
103/// # required: false,
104/// # arguments: vec![],
105/// # options: vec![],
106/// # implementation: "test_handler".to_string(),
107/// # continue_on_failure: false,
108/// # requires_success: false,
109/// # };
110/// # struct TestCommand;
111/// # impl CommandHandler for TestCommand {
112/// # fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
113/// # }
114/// registry.register_sync(definition, Box::new(TestCommand))?;
115///
116/// // Use throughout the application
117/// if let Some(handler) = registry.get_handler_sync("test") {
118/// // Execute the command
119/// }
120/// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
121/// ```
122pub struct CommandRegistry {
123 /// Map of command names to their data
124 /// Key: canonical command name
125 /// Value: (CommandDefinition, Box<dyn CommandHandler>)
126 commands: HashMap<String, (CommandDefinition, StoredHandler)>,
127
128 /// Map of aliases to canonical command names
129 /// Key: alias
130 /// Value: canonical command name
131 ///
132 /// This allows O(1) resolution of aliases to command names.
133 aliases: HashMap<String, String>,
134}
135
136impl CommandRegistry {
137 /// Create a new empty registry
138 ///
139 /// # Example
140 ///
141 /// ```
142 /// use dynamic_cli::registry::CommandRegistry;
143 ///
144 /// let registry = CommandRegistry::new();
145 /// assert_eq!(registry.list_commands().len(), 0);
146 /// ```
147 pub fn new() -> Self {
148 Self {
149 commands: HashMap::new(),
150 aliases: HashMap::new(),
151 }
152 }
153
154 /// Checks that `name` is free to use as a command name or alias.
155 ///
156 /// Shared by [`register_sync`][Self::register_sync] and
157 /// [`register_async`][Self::register_async] — a name can never belong
158 /// to both a sync and an async handler, nor be duplicated as a command
159 /// or an alias. Checked against the single unified `commands` map, so
160 /// this one call covers both storage kinds.
161 ///
162 /// # Errors
163 ///
164 /// - [`RegistryError::DuplicateRegistration`] if `name` is already a
165 /// registered command (sync or async).
166 /// - [`RegistryError::DuplicateAlias`] if `name` is already registered
167 /// as an alias of another command.
168 fn check_name_available(&self, name: &str) -> Result<()> {
169 if self.commands.contains_key(name) {
170 return Err(RegistryError::DuplicateRegistration {
171 name: name.to_string(),
172 suggestion: None,
173 }
174 .into());
175 }
176
177 if let Some(existing_cmd) = self.aliases.get(name) {
178 return Err(RegistryError::DuplicateAlias {
179 alias: name.to_string(),
180 existing_command: existing_cmd.clone(),
181 suggestion: None,
182 }
183 .into());
184 }
185
186 Ok(())
187 }
188
189 /// Registers every alias declared in `definition` as pointing to
190 /// `definition.name`. Called by both `register_sync` and
191 /// `register_async` after `check_name_available` has confirmed there is
192 /// no conflict.
193 fn insert_aliases(&mut self, definition: CommandDefinition) {
194 for alias in &definition.aliases {
195 self.aliases.insert(alias.clone(), definition.name.clone());
196 }
197 }
198
199 /// Register a command with its (sync) handler
200 ///
201 /// This method registers a command definition along with its handler.
202 /// It also registers all aliases for the command.
203 ///
204 /// Renamed from `register()` in v0.5.0 for symmetry with
205 /// [`register_async`][Self::register_async]. `register()` remains
206 /// available as a deprecated alias until v1.0.0 (DD-022).
207 ///
208 /// # Arguments
209 ///
210 /// * `definition` - The command definition from the configuration
211 /// * `handler` - The handler implementation for this command
212 ///
213 /// # Returns
214 ///
215 /// - `Ok(())` if registration succeeds
216 /// - `Err(RegistryError)` if:
217 /// - A command with the same name is already registered (sync or async)
218 /// - An alias conflicts with an existing command or alias
219 ///
220 /// # Errors
221 ///
222 /// - [`RegistryError::DuplicateRegistration`] if the command name already exists
223 /// - [`RegistryError::DuplicateAlias`] if an alias is already in use
224 ///
225 /// # Example
226 ///
227 /// ```
228 /// use dynamic_cli::registry::CommandRegistry;
229 /// use dynamic_cli::config::schema::CommandDefinition;
230 /// use dynamic_cli::executor::{CommandHandler, ParsedArgs};
231 ///
232 /// let mut registry = CommandRegistry::new();
233 ///
234 /// let definition = CommandDefinition {
235 /// name: "simulate".to_string(),
236 /// aliases: vec!["sim".to_string(), "run".to_string()],
237 /// description: "Run simulation".to_string(),
238 /// required: false,
239 /// arguments: vec![],
240 /// options: vec![],
241 /// implementation: "sim_handler".to_string(),
242 /// continue_on_failure: false,
243 /// requires_success: false,
244 /// };
245 ///
246 /// struct SimCommand;
247 /// impl CommandHandler for SimCommand {
248 /// fn execute(
249 /// &self,
250 /// _: &mut dyn dynamic_cli::context::ExecutionContext,
251 /// _: &ParsedArgs,
252 /// ) -> dynamic_cli::Result<()> {
253 /// Ok(())
254 /// }
255 /// }
256 ///
257 /// // Register the command
258 /// registry.register_sync(definition, Box::new(SimCommand))?;
259 ///
260 /// // Can now access by name or alias
261 /// assert!(registry.get_handler_sync("simulate").is_some());
262 /// assert_eq!(registry.resolve_name("sim"), Some("simulate"));
263 /// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
264 /// ```
265 pub fn register_sync(
266 &mut self,
267 definition: CommandDefinition,
268 handler: Box<dyn CommandHandler>,
269 ) -> Result<()> {
270 self.check_name_available(&definition.name)?;
271 for alias in &definition.aliases {
272 self.check_name_available(alias)?;
273 }
274 self.insert_aliases(definition.clone());
275 self.commands.insert(
276 definition.name.clone(),
277 (definition, StoredHandler::Sync(handler)),
278 );
279 Ok(())
280 }
281
282 /// Deprecated alias for [`register_sync`][Self::register_sync].
283 ///
284 /// Kept for backward compatibility with pre-0.5.0 consumers (e.g.
285 /// `chrom-rs`). Scheduled for removal in v1.0.0, batched with the other
286 /// breaking changes tracked in the v1.0.0 API cleanup issue.
287 #[deprecated(
288 since = "0.5.0",
289 note = "renamed to `register_sync` for symmetry with `register_async`; \
290 will be removed in 1.0.0"
291 )]
292 pub fn register(
293 &mut self,
294 definition: CommandDefinition,
295 handler: Box<dyn CommandHandler>,
296 ) -> Result<()> {
297 self.register_sync(definition, handler)
298 }
299
300 /// Register a command with its async handler (DD-022)
301 ///
302 /// Additive counterpart of [`register_sync`][Self::register_sync] —
303 /// same conflict-detection rules (checked against both sync and async
304 /// registrations sharing the unified internal storage, plus aliases),
305 /// same alias handling.
306 ///
307 /// # Errors
308 ///
309 /// - [`RegistryError::DuplicateRegistration`] if the command name already exists
310 /// - [`RegistryError::DuplicateAlias`] if an alias is already in use
311 ///
312 /// # Example
313 ///
314 /// ```
315 /// use dynamic_cli::registry::CommandRegistry;
316 /// use dynamic_cli::config::schema::CommandDefinition;
317 /// use dynamic_cli::executor::{AsyncCommandHandler, ParsedArgs};
318 /// use async_trait::async_trait;
319 ///
320 /// let mut registry = CommandRegistry::new();
321 ///
322 /// let definition = CommandDefinition {
323 /// name: "fetch".to_string(),
324 /// aliases: vec![],
325 /// description: "Fetch remote data".to_string(),
326 /// required: false,
327 /// arguments: vec![],
328 /// options: vec![],
329 /// implementation: "fetch_handler".to_string(),
330 /// continue_on_failure: false,
331 /// requires_success: false,
332 /// };
333 ///
334 /// struct FetchCommand;
335 /// #[async_trait]
336 /// impl AsyncCommandHandler for FetchCommand {
337 /// async fn execute(
338 /// &self,
339 /// _: &mut dyn dynamic_cli::context::ExecutionContext,
340 /// _: &ParsedArgs,
341 /// ) -> dynamic_cli::Result<()> {
342 /// Ok(())
343 /// }
344 /// }
345 ///
346 /// registry.register_async(definition, Box::new(FetchCommand))?;
347 /// assert!(registry.get_handler_async("fetch").is_some());
348 /// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
349 /// ```
350 pub fn register_async(
351 &mut self,
352 definition: CommandDefinition,
353 handler: Box<dyn AsyncCommandHandler>,
354 ) -> Result<()> {
355 self.check_name_available(&definition.name)?;
356 for alias in &definition.aliases {
357 self.check_name_available(alias)?;
358 }
359 self.insert_aliases(definition.clone());
360 self.commands.insert(
361 definition.name.clone(),
362 (definition, StoredHandler::Async(handler)),
363 );
364 Ok(())
365 }
366
367 /// Resolve a name (command or alias) to the canonical command name
368 ///
369 /// This method checks if the given name is either:
370 /// - A registered command name (returns the name itself)
371 /// - An alias (returns the canonical command name)
372 ///
373 /// # Arguments
374 ///
375 /// * `name` - The name or alias to resolve
376 ///
377 /// # Returns
378 ///
379 /// - `Some(&str)` - The canonical command name
380 /// - `None` - If the name is not registered
381 ///
382 /// # Example
383 ///
384 /// ```
385 /// use dynamic_cli::registry::CommandRegistry;
386 /// # use dynamic_cli::config::schema::CommandDefinition;
387 /// # use dynamic_cli::executor::CommandHandler;
388 /// # use std::collections::HashMap;
389 ///
390 /// let mut registry = CommandRegistry::new();
391 ///
392 /// # let definition = CommandDefinition {
393 /// # name: "hello".to_string(),
394 /// # aliases: vec!["hi".to_string()],
395 /// # description: "".to_string(),
396 /// # required: false,
397 /// # arguments: vec![],
398 /// # options: vec![],
399 /// # implementation: "".to_string(),
400 /// # continue_on_failure: false,
401 /// # requires_success: false,
402 /// # };
403 /// # struct TestCmd;
404 /// # impl CommandHandler for TestCmd {
405 /// # fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
406 /// # }
407 /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
408 /// // Resolve command name
409 /// assert_eq!(registry.resolve_name("hello"), Some("hello"));
410 ///
411 /// // Resolve alias
412 /// assert_eq!(registry.resolve_name("hi"), Some("hello"));
413 ///
414 /// // Unknown name
415 /// assert_eq!(registry.resolve_name("unknown"), None);
416 /// ```
417 pub fn resolve_name(&self, name: &str) -> Option<&str> {
418 // First check if it's a command name
419 // Return reference to the stored name, not the parameter
420 if let Some((cmd_def, _)) = self.commands.get(name) {
421 return Some(cmd_def.name.as_str());
422 }
423
424 // Then check if it's an alias
425 self.aliases.get(name).map(|s| s.as_str())
426 }
427
428 /// Get the definition of a command by name or alias
429 ///
430 /// # Arguments
431 ///
432 /// * `name` - The command name or alias
433 ///
434 /// # Returns
435 ///
436 /// - `Some(&CommandDefinition)` if the command exists
437 /// - `None` if the command is not registered
438 ///
439 /// # Example
440 ///
441 /// ```
442 /// # use dynamic_cli::registry::CommandRegistry;
443 /// # use dynamic_cli::config::schema::CommandDefinition;
444 /// # use dynamic_cli::executor::CommandHandler;
445 /// # use std::collections::HashMap;
446 /// # let mut registry = CommandRegistry::new();
447 /// # let definition = CommandDefinition {
448 /// # name: "test".to_string(),
449 /// # aliases: vec!["t".to_string()],
450 /// # description: "Test command".to_string(),
451 /// # required: false,
452 /// # arguments: vec![],
453 /// # options: vec![],
454 /// # implementation: "".to_string(),
455 /// # continue_on_failure: false,
456 /// # requires_success: false,
457 /// # };
458 /// # struct TestCmd;
459 /// # impl CommandHandler for TestCmd {
460 /// # fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
461 /// # }
462 /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
463 /// // Get by name
464 /// if let Some(def) = registry.get_definition("test") {
465 /// assert_eq!(def.name, "test");
466 /// assert_eq!(def.description, "Test command");
467 /// }
468 ///
469 /// // Get by alias
470 /// if let Some(def) = registry.get_definition("t") {
471 /// assert_eq!(def.name, "test");
472 /// }
473 /// ```
474 pub fn get_definition(&self, name: &str) -> Option<&CommandDefinition> {
475 let canonical_name = self.resolve_name(name)?;
476 self.commands.get(canonical_name).map(|(def, _)| def)
477 }
478
479 /// Get the (sync) handler of a command by name or alias
480 ///
481 /// This is the primary method used during CLI/REPL dispatch to
482 /// retrieve the handler that will execute the command. Returns `None`
483 /// both when the name isn't registered at all, and when it resolves to
484 /// an *async* handler (query [`get_handler_async`][Self::get_handler_async]
485 /// instead in that case) — dispatch sites try both in sequence.
486 ///
487 /// Renamed from `get_handler()` in v0.5.0 for symmetry with
488 /// [`get_handler_async`][Self::get_handler_async]. `get_handler()`
489 /// remains available as a deprecated alias until v1.0.0 (DD-022).
490 ///
491 /// # Arguments
492 ///
493 /// * `name` - The command name or alias
494 ///
495 /// # Returns
496 ///
497 /// - `Some(&dyn CommandHandler)` if a sync handler is registered under this name
498 /// - `None` if unregistered, or if registered as an async handler
499 ///
500 /// # Example
501 ///
502 /// ```
503 /// # use dynamic_cli::registry::CommandRegistry;
504 /// # use dynamic_cli::config::schema::CommandDefinition;
505 /// # use dynamic_cli::executor::CommandHandler;
506 /// # use std::collections::HashMap;
507 /// # let mut registry = CommandRegistry::new();
508 /// # let definition = CommandDefinition {
509 /// # name: "exec".to_string(),
510 /// # aliases: vec!["x".to_string()],
511 /// # description: "".to_string(),
512 /// # required: false,
513 /// # arguments: vec![],
514 /// # options: vec![],
515 /// # implementation: "".to_string(),
516 /// # continue_on_failure: false,
517 /// # requires_success: false,
518 /// # };
519 /// # struct ExecCmd;
520 /// # impl CommandHandler for ExecCmd {
521 /// # fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
522 /// # }
523 /// # registry.register_sync(definition, Box::new(ExecCmd)).unwrap();
524 /// // Get handler by name
525 /// if let Some(handler) = registry.get_handler_sync("exec") {
526 /// // Use handler for execution
527 /// }
528 ///
529 /// // Get handler by alias
530 /// if let Some(handler) = registry.get_handler_sync("x") {
531 /// // Same handler
532 /// }
533 /// ```
534 // The return type &dyn CommandHandler is intentional: callers receive a
535 // reference to the handler, which preserves the indirection needed for
536 // dynamic dispatch without transferring ownership.
537 pub fn get_handler_sync(&self, name: &str) -> Option<&dyn CommandHandler> {
538 let canonical = self.resolve_name(name)?;
539 match &self.commands.get(canonical)?.1 {
540 StoredHandler::Sync(h) => Some(h.as_ref()),
541 StoredHandler::Async(_) => None,
542 }
543 }
544
545 /// Deprecated alias for [`get_handler_sync`][Self::get_handler_sync].
546 /// Scheduled for removal in v1.0.0.
547 #[deprecated(
548 since = "0.5.0",
549 note = "renamed to `get_handler_sync` for symmetry with `get_handler_async`; \
550 will be removed in 1.0.0"
551 )]
552 pub fn get_handler(&self, name: &str) -> Option<&dyn CommandHandler> {
553 self.get_handler_sync(name)
554 }
555
556 /// Get the async handler of a command by name or alias (DD-022)
557 ///
558 /// Additive counterpart of [`get_handler_sync`][Self::get_handler_sync].
559 /// Returns `None` both when the name isn't registered at all, and when
560 /// it resolves to a *sync* handler.
561 ///
562 /// # Example
563 ///
564 /// ```
565 /// # use dynamic_cli::registry::CommandRegistry;
566 /// # use dynamic_cli::config::schema::CommandDefinition;
567 /// # use dynamic_cli::executor::AsyncCommandHandler;
568 /// # use std::collections::HashMap;
569 /// # use async_trait::async_trait;
570 /// # let mut registry = CommandRegistry::new();
571 /// # let definition = CommandDefinition {
572 /// # name: "fetch".to_string(),
573 /// # aliases: vec![],
574 /// # description: "".to_string(),
575 /// # required: false,
576 /// # arguments: vec![],
577 /// # options: vec![],
578 /// # implementation: "".to_string(),
579 /// # continue_on_failure: false,
580 /// # requires_success: false,
581 /// # };
582 /// # struct FetchCmd;
583 /// # #[async_trait]
584 /// # impl AsyncCommandHandler for FetchCmd {
585 /// # async fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
586 /// # }
587 /// # registry.register_async(definition, Box::new(FetchCmd)).unwrap();
588 /// assert!(registry.get_handler_async("fetch").is_some());
589 /// assert!(registry.get_handler_sync("fetch").is_none()); // wrong accessor
590 /// ```
591 pub fn get_handler_async(&self, name: &str) -> Option<&dyn AsyncCommandHandler> {
592 let canonical = self.resolve_name(name)?;
593 match &self.commands.get(canonical)?.1 {
594 StoredHandler::Async(h) => Some(h.as_ref()),
595 StoredHandler::Sync(_) => None,
596 }
597 }
598
599 /// List all registered command definitions
600 ///
601 /// Returns a vector of references to all command definitions in the registry.
602 /// The order is not guaranteed.
603 ///
604 /// # Returns
605 ///
606 /// Vector of command definition references
607 ///
608 /// # Example
609 ///
610 /// ```
611 /// # use dynamic_cli::registry::CommandRegistry;
612 /// # use dynamic_cli::config::schema::CommandDefinition;
613 /// # use dynamic_cli::executor::CommandHandler;
614 /// # use std::collections::HashMap;
615 /// # let mut registry = CommandRegistry::new();
616 /// # let def1 = CommandDefinition {
617 /// # name: "cmd1".to_string(),
618 /// # aliases: vec![],
619 /// # description: "".to_string(),
620 /// # required: false,
621 /// # arguments: vec![],
622 /// # options: vec![],
623 /// # implementation: "".to_string(),
624 /// # continue_on_failure: false,
625 /// # requires_success: false,
626 /// # };
627 /// # let def2 = CommandDefinition {
628 /// # name: "cmd2".to_string(),
629 /// # aliases: vec![],
630 /// # description: "".to_string(),
631 /// # required: false,
632 /// # arguments: vec![],
633 /// # options: vec![],
634 /// # implementation: "".to_string(),
635 /// # continue_on_failure: false,
636 /// # requires_success: false,
637 /// # };
638 /// # struct TestCmd;
639 /// # impl CommandHandler for TestCmd {
640 /// # fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
641 /// # }
642 /// # registry.register_sync(def1, Box::new(TestCmd)).unwrap();
643 /// # registry.register_sync(def2, Box::new(TestCmd)).unwrap();
644 /// let commands = registry.list_commands();
645 /// assert_eq!(commands.len(), 2);
646 ///
647 /// // Use for help text, command completion, etc.
648 /// for cmd in commands {
649 /// println!("{}: {}", cmd.name, cmd.description);
650 /// }
651 /// ```
652 pub fn list_commands(&self) -> Vec<&CommandDefinition> {
653 self.commands.values().map(|(def, _)| def).collect()
654 }
655
656 /// Get the number of registered commands
657 ///
658 /// # Example
659 ///
660 /// ```
661 /// use dynamic_cli::registry::CommandRegistry;
662 ///
663 /// let registry = CommandRegistry::new();
664 /// assert_eq!(registry.len(), 0);
665 /// ```
666 pub fn len(&self) -> usize {
667 self.commands.len()
668 }
669
670 /// Check if the registry is empty
671 ///
672 /// # Example
673 ///
674 /// ```
675 /// use dynamic_cli::registry::CommandRegistry;
676 ///
677 /// let registry = CommandRegistry::new();
678 /// assert!(registry.is_empty());
679 /// ```
680 pub fn is_empty(&self) -> bool {
681 self.commands.is_empty()
682 }
683
684 /// Check if a command is registered (by name or alias)
685 ///
686 /// # Example
687 ///
688 /// ```
689 /// # use dynamic_cli::registry::CommandRegistry;
690 /// # use dynamic_cli::config::schema::CommandDefinition;
691 /// # use dynamic_cli::executor::CommandHandler;
692 /// # use std::collections::HashMap;
693 /// # let mut registry = CommandRegistry::new();
694 /// # let definition = CommandDefinition {
695 /// # name: "test".to_string(),
696 /// # aliases: vec!["t".to_string()],
697 /// # description: "".to_string(),
698 /// # required: false,
699 /// # arguments: vec![],
700 /// # options: vec![],
701 /// # implementation: "".to_string(),
702 /// # continue_on_failure: false,
703 /// # requires_success: false,
704 /// # };
705 /// # struct TestCmd;
706 /// # impl CommandHandler for TestCmd {
707 /// # fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
708 /// # }
709 /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
710 /// assert!(registry.contains("test"));
711 /// assert!(registry.contains("t"));
712 /// assert!(!registry.contains("unknown"));
713 /// ```
714 pub fn contains(&self, name: &str) -> bool {
715 self.resolve_name(name).is_some()
716 }
717}
718
719// Implement Default for convenience
720impl Default for CommandRegistry {
721 fn default() -> Self {
722 Self::new()
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use crate::parser::ParsedArgs;
730
731 // Test fixtures
732
733 struct TestHandler;
734
735 impl CommandHandler for TestHandler {
736 fn execute(
737 &self,
738 _context: &mut dyn crate::context::ExecutionContext,
739 _args: &ParsedArgs,
740 ) -> crate::error::Result<()> {
741 Ok(())
742 }
743 }
744
745 struct TestAsyncHandler;
746
747 #[async_trait::async_trait]
748 impl AsyncCommandHandler for TestAsyncHandler {
749 async fn execute(
750 &self,
751 _context: &mut dyn crate::context::ExecutionContext,
752 _args: &ParsedArgs,
753 ) -> crate::error::Result<()> {
754 Ok(())
755 }
756 }
757
758 fn create_test_definition(name: &str, aliases: Vec<&str>) -> CommandDefinition {
759 CommandDefinition {
760 name: name.to_string(),
761 aliases: aliases.iter().map(|s| s.to_string()).collect(),
762 description: format!("{} command", name),
763 required: false,
764 arguments: vec![],
765 options: vec![],
766 implementation: format!("{}_handler", name),
767 continue_on_failure: false,
768 requires_success: false,
769 }
770 }
771
772 // Basic functionality tests
773 #[test]
774 fn test_new_registry_is_empty() {
775 let registry = CommandRegistry::new();
776 assert!(registry.is_empty());
777 assert_eq!(registry.len(), 0);
778 assert_eq!(registry.list_commands().len(), 0);
779 }
780
781 #[test]
782 fn test_register_command() {
783 let mut registry = CommandRegistry::new();
784 let definition = create_test_definition("test", vec![]);
785
786 let result = registry.register_sync(definition, Box::new(TestHandler));
787
788 assert!(result.is_ok());
789 assert_eq!(registry.len(), 1);
790 assert!(!registry.is_empty());
791 }
792
793 /// Deprecated-alias coverage (DD-022 companion issue): `register()` and
794 /// `get_handler()` must keep behaving exactly like `register_sync()` /
795 /// `get_handler_sync()` until they're removed in v1.0.0. This is the
796 /// only place in the crate allowed to call them directly.
797 #[test]
798 #[allow(deprecated)]
799 fn test_deprecated_register_alias_still_works() {
800 let mut registry = CommandRegistry::new();
801 let definition = create_test_definition("legacy", vec!["old"]);
802
803 let result = registry.register(definition, Box::new(TestHandler));
804
805 assert!(result.is_ok());
806 assert!(registry.get_handler("legacy").is_some());
807 assert!(registry.get_handler("old").is_some());
808 assert_eq!(registry.resolve_name("old"), Some("legacy"));
809 }
810
811 #[test]
812 fn test_register_command_with_aliases() {
813 let mut registry = CommandRegistry::new();
814 let definition = create_test_definition("hello", vec!["hi", "greet"]);
815
816 registry
817 .register_sync(definition, Box::new(TestHandler))
818 .unwrap();
819
820 assert_eq!(registry.len(), 1);
821 assert!(registry.contains("hello"));
822 assert!(registry.contains("hi"));
823 assert!(registry.contains("greet"));
824 }
825
826 #[test]
827 fn test_register_duplicate_command_fails() {
828 let mut registry = CommandRegistry::new();
829 let def1 = create_test_definition("test", vec![]);
830 let def2 = create_test_definition("test", vec![]);
831
832 registry.register_sync(def1, Box::new(TestHandler)).unwrap();
833 let result = registry.register_sync(def2, Box::new(TestHandler));
834
835 assert!(result.is_err());
836 match result.unwrap_err() {
837 crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
838 name,
839 ..
840 }) => {
841 assert_eq!(name, "test");
842 }
843 _ => panic!("Wrong error type"),
844 }
845 }
846
847 #[test]
848 fn test_register_duplicate_alias_fails() {
849 let mut registry = CommandRegistry::new();
850 let def1 = create_test_definition("cmd1", vec!["c"]);
851 let def2 = create_test_definition("cmd2", vec!["c"]);
852
853 registry.register_sync(def1, Box::new(TestHandler)).unwrap();
854 let result = registry.register_sync(def2, Box::new(TestHandler));
855
856 assert!(result.is_err());
857 match result.unwrap_err() {
858 crate::error::DynamicCliError::Registry(RegistryError::DuplicateAlias {
859 alias,
860 existing_command,
861 ..
862 }) => {
863 assert_eq!(alias, "c");
864 assert_eq!(existing_command, "cmd1");
865 }
866 _ => panic!("Wrong error type"),
867 }
868 }
869
870 #[test]
871 fn test_alias_conflicts_with_command_name() {
872 let mut registry = CommandRegistry::new();
873 let def1 = create_test_definition("test", vec![]);
874 let def2 = create_test_definition("other", vec!["test"]);
875
876 registry.register_sync(def1, Box::new(TestHandler)).unwrap();
877 let result = registry.register_sync(def2, Box::new(TestHandler));
878
879 assert!(result.is_err());
880 }
881
882 #[test]
883 fn test_command_name_conflicts_with_alias() {
884 let mut registry = CommandRegistry::new();
885 let def1 = create_test_definition("cmd1", vec!["other"]);
886 let def2 = create_test_definition("other", vec![]);
887
888 registry.register_sync(def1, Box::new(TestHandler)).unwrap();
889 let result = registry.register_sync(def2, Box::new(TestHandler));
890
891 assert!(result.is_err());
892 }
893
894 // Resolve name tests
895 #[test]
896 fn test_resolve_command_name() {
897 let mut registry = CommandRegistry::new();
898 let definition = create_test_definition("test", vec![]);
899
900 registry
901 .register_sync(definition, Box::new(TestHandler))
902 .unwrap();
903
904 assert_eq!(registry.resolve_name("test"), Some("test"));
905 }
906
907 #[test]
908 fn test_resolve_alias() {
909 let mut registry = CommandRegistry::new();
910 let definition = create_test_definition("hello", vec!["hi", "greet"]);
911
912 registry
913 .register_sync(definition, Box::new(TestHandler))
914 .unwrap();
915
916 assert_eq!(registry.resolve_name("hi"), Some("hello"));
917 assert_eq!(registry.resolve_name("greet"), Some("hello"));
918 }
919
920 #[test]
921 fn test_resolve_unknown_name() {
922 let registry = CommandRegistry::new();
923 assert_eq!(registry.resolve_name("unknown"), None);
924 }
925
926 // Get definition tests
927 #[test]
928 fn test_get_definition_by_name() {
929 let mut registry = CommandRegistry::new();
930 let definition = create_test_definition("test", vec![]);
931
932 registry
933 .register_sync(definition, Box::new(TestHandler))
934 .unwrap();
935
936 let retrieved = registry.get_definition("test");
937 assert!(retrieved.is_some());
938 assert_eq!(retrieved.unwrap().name, "test");
939 }
940
941 #[test]
942 fn test_get_definition_by_alias() {
943 let mut registry = CommandRegistry::new();
944 let definition = create_test_definition("hello", vec!["hi"]);
945
946 registry
947 .register_sync(definition, Box::new(TestHandler))
948 .unwrap();
949
950 let retrieved = registry.get_definition("hi");
951 assert!(retrieved.is_some());
952 assert_eq!(retrieved.unwrap().name, "hello");
953 }
954
955 #[test]
956 fn test_get_definition_unknown() {
957 let registry = CommandRegistry::new();
958 assert!(registry.get_definition("unknown").is_none());
959 }
960
961 // Get handler tests
962 #[test]
963 fn test_get_handler_by_name() {
964 let mut registry = CommandRegistry::new();
965 let definition = create_test_definition("test", vec![]);
966
967 registry
968 .register_sync(definition, Box::new(TestHandler))
969 .unwrap();
970
971 let handler = registry.get_handler_sync("test");
972 assert!(handler.is_some());
973 }
974
975 #[test]
976 fn test_get_handler_sync_by_name() {
977 let mut registry = CommandRegistry::new();
978 let definition = create_test_definition("test", vec![]);
979
980 registry
981 .register_sync(definition, Box::new(TestHandler))
982 .unwrap();
983
984 let handler = registry.get_handler_sync("test");
985 assert!(handler.is_some());
986 }
987
988 #[test]
989 fn test_get_handler_by_alias() {
990 let mut registry = CommandRegistry::new();
991 let definition = create_test_definition("hello", vec!["hi"]);
992
993 registry
994 .register_sync(definition, Box::new(TestHandler))
995 .unwrap();
996
997 let handler = registry.get_handler_sync("hi");
998 assert!(handler.is_some());
999 }
1000
1001 #[test]
1002 fn test_get_handler_unknown() {
1003 let registry = CommandRegistry::new();
1004 assert!(registry.get_handler_sync("unknown").is_none());
1005 }
1006
1007 // List commands tests
1008 #[test]
1009 fn test_list_commands_empty() {
1010 let registry = CommandRegistry::new();
1011 let commands = registry.list_commands();
1012 assert_eq!(commands.len(), 0);
1013 }
1014
1015 #[test]
1016 fn test_list_commands_multiple() {
1017 let mut registry = CommandRegistry::new();
1018
1019 registry
1020 .register_sync(
1021 create_test_definition("cmd1", vec![]),
1022 Box::new(TestHandler),
1023 )
1024 .unwrap();
1025 registry
1026 .register_sync(
1027 create_test_definition("cmd2", vec![]),
1028 Box::new(TestHandler),
1029 )
1030 .unwrap();
1031 registry
1032 .register_sync(
1033 create_test_definition("cmd3", vec![]),
1034 Box::new(TestHandler),
1035 )
1036 .unwrap();
1037
1038 let commands = registry.list_commands();
1039 assert_eq!(commands.len(), 3);
1040
1041 let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1042 assert!(names.contains(&"cmd1"));
1043 assert!(names.contains(&"cmd2"));
1044 assert!(names.contains(&"cmd3"));
1045 }
1046
1047 // Integration tests
1048 #[test]
1049 fn test_complete_workflow() {
1050 let mut registry = CommandRegistry::new();
1051
1052 // Register multiple commands with aliases
1053 let def1 = create_test_definition("simulate", vec!["sim", "run"]);
1054 let def2 = create_test_definition("validate", vec!["val", "check"]);
1055 let def3 = create_test_definition("help", vec!["h", "?"]);
1056
1057 registry.register_sync(def1, Box::new(TestHandler)).unwrap();
1058 registry.register_sync(def2, Box::new(TestHandler)).unwrap();
1059 registry.register_sync(def3, Box::new(TestHandler)).unwrap();
1060
1061 // Verify registry state
1062 assert_eq!(registry.len(), 3);
1063
1064 // Verify all names resolve correctly
1065 assert_eq!(registry.resolve_name("simulate"), Some("simulate"));
1066 assert_eq!(registry.resolve_name("sim"), Some("simulate"));
1067 assert_eq!(registry.resolve_name("validate"), Some("validate"));
1068 assert_eq!(registry.resolve_name("val"), Some("validate"));
1069
1070 // Verify handlers are accessible
1071 assert!(registry.get_handler_sync("simulate").is_some());
1072 assert!(registry.get_handler_sync("sim").is_some());
1073 assert!(registry.get_handler_sync("h").is_some());
1074
1075 // Verify definitions are accessible
1076 let sim_def = registry.get_definition("sim");
1077 assert!(sim_def.is_some());
1078 assert_eq!(sim_def.unwrap().name, "simulate");
1079 }
1080
1081 #[test]
1082 fn test_default_trait() {
1083 let registry: CommandRegistry = Default::default();
1084 assert!(registry.is_empty());
1085 }
1086
1087 #[test]
1088 fn test_contains_method() {
1089 let mut registry = CommandRegistry::new();
1090 let definition = create_test_definition("test", vec!["t"]);
1091
1092 registry
1093 .register_sync(definition, Box::new(TestHandler))
1094 .unwrap();
1095
1096 assert!(registry.contains("test"));
1097 assert!(registry.contains("t"));
1098 assert!(!registry.contains("unknown"));
1099 }
1100
1101 #[test]
1102 fn test_multiple_aliases_same_command() {
1103 let mut registry = CommandRegistry::new();
1104 let definition = create_test_definition("command", vec!["c", "cmd", "com"]);
1105
1106 registry
1107 .register_sync(definition, Box::new(TestHandler))
1108 .unwrap();
1109
1110 // All aliases should resolve to the same command
1111 assert_eq!(registry.resolve_name("c"), Some("command"));
1112 assert_eq!(registry.resolve_name("cmd"), Some("command"));
1113 assert_eq!(registry.resolve_name("com"), Some("command"));
1114
1115 // All should return the same handler
1116 let handler1 = registry.get_handler_sync("c");
1117 let handler2 = registry.get_handler_sync("cmd");
1118 assert!(handler1.is_some());
1119 assert!(handler2.is_some());
1120 }
1121
1122 #[test]
1123 fn test_case_sensitivity() {
1124 let mut registry = CommandRegistry::new();
1125 let definition = create_test_definition("Test", vec![]);
1126
1127 registry
1128 .register_sync(definition, Box::new(TestHandler))
1129 .unwrap();
1130
1131 // Case matters
1132 assert!(registry.contains("Test"));
1133 assert!(!registry.contains("test"));
1134 assert!(!registry.contains("TEST"));
1135 }
1136
1137 #[test]
1138 fn test_empty_alias_list() {
1139 let mut registry = CommandRegistry::new();
1140 let definition = create_test_definition("test", vec![]);
1141
1142 let result = registry.register_sync(definition, Box::new(TestHandler));
1143
1144 assert!(result.is_ok());
1145 assert!(registry.contains("test"));
1146 }
1147
1148 // ============================================================================
1149 // AsyncCommandHandler / register_async / get_handler_async TESTS (DD-022)
1150 // ============================================================================
1151
1152 #[test]
1153 fn test_register_async_command() {
1154 let mut registry = CommandRegistry::new();
1155 let definition = create_test_definition("fetch", vec![]);
1156
1157 let result = registry.register_async(definition, Box::new(TestAsyncHandler));
1158
1159 assert!(result.is_ok());
1160 assert_eq!(registry.len(), 1);
1161 }
1162
1163 #[test]
1164 fn test_register_async_command_with_aliases() {
1165 let mut registry = CommandRegistry::new();
1166 let definition = create_test_definition("fetch", vec!["f", "get-remote"]);
1167
1168 registry
1169 .register_async(definition, Box::new(TestAsyncHandler))
1170 .unwrap();
1171
1172 assert!(registry.contains("fetch"));
1173 assert!(registry.contains("f"));
1174 assert!(registry.contains("get-remote"));
1175 assert_eq!(registry.resolve_name("f"), Some("fetch"));
1176 }
1177
1178 #[test]
1179 fn test_get_handler_async_by_name_and_alias() {
1180 let mut registry = CommandRegistry::new();
1181 let definition = create_test_definition("fetch", vec!["f"]);
1182
1183 registry
1184 .register_async(definition, Box::new(TestAsyncHandler))
1185 .unwrap();
1186
1187 assert!(registry.get_handler_async("fetch").is_some());
1188 assert!(registry.get_handler_async("f").is_some());
1189 assert!(registry.get_handler_async("unknown").is_none());
1190 }
1191
1192 /// The core cross-accessor guarantee DD-022 depends on: querying an
1193 /// async-registered command through the *sync* accessor returns `None`
1194 /// (not the wrong handler, not a panic) — dispatch sites rely on this
1195 /// to fall through from `get_handler_sync` to `get_handler_async`.
1196 #[test]
1197 fn test_sync_accessor_returns_none_for_async_command() {
1198 let mut registry = CommandRegistry::new();
1199 let definition = create_test_definition("fetch", vec![]);
1200
1201 registry
1202 .register_async(definition, Box::new(TestAsyncHandler))
1203 .unwrap();
1204
1205 assert!(registry.get_handler_sync("fetch").is_none());
1206 assert!(registry.get_handler_async("fetch").is_some());
1207 }
1208
1209 /// Symmetric case: querying a sync-registered command through the
1210 /// *async* accessor returns `None`.
1211 #[test]
1212 fn test_async_accessor_returns_none_for_sync_command() {
1213 let mut registry = CommandRegistry::new();
1214 let definition = create_test_definition("test", vec![]);
1215
1216 registry
1217 .register_sync(definition, Box::new(TestHandler))
1218 .unwrap();
1219
1220 assert!(registry.get_handler_async("test").is_none());
1221 assert!(registry.get_handler_sync("test").is_some());
1222 }
1223
1224 /// A command name already taken by a sync handler must be rejected for
1225 /// async registration — the unified storage means one name, one kind.
1226 #[test]
1227 fn test_register_async_conflicts_with_existing_sync_name() {
1228 let mut registry = CommandRegistry::new();
1229 let sync_def = create_test_definition("dual", vec![]);
1230 let async_def = create_test_definition("dual", vec![]);
1231
1232 registry
1233 .register_sync(sync_def, Box::new(TestHandler))
1234 .unwrap();
1235 let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1236
1237 assert!(result.is_err());
1238 match result.unwrap_err() {
1239 crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
1240 name,
1241 ..
1242 }) => {
1243 assert_eq!(name, "dual");
1244 }
1245 other => panic!("Expected DuplicateRegistration, got: {:?}", other),
1246 }
1247 }
1248
1249 /// Symmetric case: a name already taken by an async handler must be
1250 /// rejected for sync registration.
1251 #[test]
1252 fn test_register_sync_conflicts_with_existing_async_name() {
1253 let mut registry = CommandRegistry::new();
1254 let async_def = create_test_definition("dual", vec![]);
1255 let sync_def = create_test_definition("dual", vec![]);
1256
1257 registry
1258 .register_async(async_def, Box::new(TestAsyncHandler))
1259 .unwrap();
1260 let result = registry.register_sync(sync_def, Box::new(TestHandler));
1261
1262 assert!(result.is_err());
1263 }
1264
1265 /// An async command's alias must not collide with an existing sync
1266 /// command's alias, and vice versa — conflict detection is shared
1267 /// across both kinds via `check_name_available`.
1268 #[test]
1269 fn test_async_alias_conflicts_with_sync_alias() {
1270 let mut registry = CommandRegistry::new();
1271 let sync_def = create_test_definition("cmd1", vec!["shared"]);
1272 let async_def = create_test_definition("cmd2", vec!["shared"]);
1273
1274 registry
1275 .register_sync(sync_def, Box::new(TestHandler))
1276 .unwrap();
1277 let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1278
1279 assert!(result.is_err());
1280 }
1281
1282 #[test]
1283 fn test_get_definition_works_for_async_command() {
1284 let mut registry = CommandRegistry::new();
1285 let definition = create_test_definition("fetch", vec!["f"]);
1286
1287 registry
1288 .register_async(definition, Box::new(TestAsyncHandler))
1289 .unwrap();
1290
1291 let retrieved = registry.get_definition("f");
1292 assert!(retrieved.is_some());
1293 assert_eq!(retrieved.unwrap().name, "fetch");
1294 }
1295
1296 #[test]
1297 fn test_list_commands_includes_both_sync_and_async() {
1298 let mut registry = CommandRegistry::new();
1299
1300 registry
1301 .register_sync(
1302 create_test_definition("sync-cmd", vec![]),
1303 Box::new(TestHandler),
1304 )
1305 .unwrap();
1306 registry
1307 .register_async(
1308 create_test_definition("async-cmd", vec![]),
1309 Box::new(TestAsyncHandler),
1310 )
1311 .unwrap();
1312
1313 let commands = registry.list_commands();
1314 assert_eq!(commands.len(), 2);
1315 let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1316 assert!(names.contains(&"sync-cmd"));
1317 assert!(names.contains(&"async-cmd"));
1318 }
1319
1320 #[test]
1321 fn test_mixed_registry_workflow() {
1322 // End-to-end: a registry with both sync and async commands behaves
1323 // consistently across resolve_name / get_definition / len / contains.
1324 let mut registry = CommandRegistry::new();
1325
1326 registry
1327 .register_sync(
1328 create_test_definition("simulate", vec!["sim"]),
1329 Box::new(TestHandler),
1330 )
1331 .unwrap();
1332 registry
1333 .register_async(
1334 create_test_definition("fetch", vec!["f"]),
1335 Box::new(TestAsyncHandler),
1336 )
1337 .unwrap();
1338
1339 assert_eq!(registry.len(), 2);
1340 assert!(registry.contains("sim"));
1341 assert!(registry.contains("f"));
1342
1343 assert!(registry.get_handler_sync("simulate").is_some());
1344 assert!(registry.get_handler_async("fetch").is_some());
1345 assert!(registry.get_handler_sync("fetch").is_none());
1346 assert!(registry.get_handler_async("simulate").is_none());
1347 }
1348}