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