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
709 // Test fixtures
710
711 struct TestHandler;
712
713 impl CommandHandler for TestHandler {
714 fn execute(
715 &self,
716 _context: &mut dyn crate::context::ExecutionContext,
717 _args: &ParsedArgs,
718 ) -> crate::error::Result<()> {
719 Ok(())
720 }
721 }
722
723 struct TestAsyncHandler;
724
725 #[async_trait::async_trait]
726 impl AsyncCommandHandler for TestAsyncHandler {
727 async fn execute(
728 &self,
729 _context: &mut dyn crate::context::ExecutionContext,
730 _args: &ParsedArgs,
731 ) -> crate::error::Result<()> {
732 Ok(())
733 }
734 }
735
736 fn create_test_definition(name: &str, aliases: Vec<&str>) -> CommandDefinition {
737 CommandDefinition {
738 name: name.to_string(),
739 aliases: aliases.iter().map(|s| s.to_string()).collect(),
740 description: format!("{} command", name),
741 required: false,
742 arguments: vec![],
743 options: vec![],
744 implementation: format!("{}_handler", name),
745 }
746 }
747
748 // Basic functionality tests
749 #[test]
750 fn test_new_registry_is_empty() {
751 let registry = CommandRegistry::new();
752 assert!(registry.is_empty());
753 assert_eq!(registry.len(), 0);
754 assert_eq!(registry.list_commands().len(), 0);
755 }
756
757 #[test]
758 fn test_register_command() {
759 let mut registry = CommandRegistry::new();
760 let definition = create_test_definition("test", vec![]);
761
762 let result = registry.register_sync(definition, Box::new(TestHandler));
763
764 assert!(result.is_ok());
765 assert_eq!(registry.len(), 1);
766 assert!(!registry.is_empty());
767 }
768
769 /// Deprecated-alias coverage (DD-022 companion issue): `register()` and
770 /// `get_handler()` must keep behaving exactly like `register_sync()` /
771 /// `get_handler_sync()` until they're removed in v1.0.0. This is the
772 /// only place in the crate allowed to call them directly.
773 #[test]
774 #[allow(deprecated)]
775 fn test_deprecated_register_alias_still_works() {
776 let mut registry = CommandRegistry::new();
777 let definition = create_test_definition("legacy", vec!["old"]);
778
779 let result = registry.register(definition, Box::new(TestHandler));
780
781 assert!(result.is_ok());
782 assert!(registry.get_handler("legacy").is_some());
783 assert!(registry.get_handler("old").is_some());
784 assert_eq!(registry.resolve_name("old"), Some("legacy"));
785 }
786
787 #[test]
788 fn test_register_command_with_aliases() {
789 let mut registry = CommandRegistry::new();
790 let definition = create_test_definition("hello", vec!["hi", "greet"]);
791
792 registry
793 .register_sync(definition, Box::new(TestHandler))
794 .unwrap();
795
796 assert_eq!(registry.len(), 1);
797 assert!(registry.contains("hello"));
798 assert!(registry.contains("hi"));
799 assert!(registry.contains("greet"));
800 }
801
802 #[test]
803 fn test_register_duplicate_command_fails() {
804 let mut registry = CommandRegistry::new();
805 let def1 = create_test_definition("test", vec![]);
806 let def2 = create_test_definition("test", vec![]);
807
808 registry.register_sync(def1, Box::new(TestHandler)).unwrap();
809 let result = registry.register_sync(def2, Box::new(TestHandler));
810
811 assert!(result.is_err());
812 match result.unwrap_err() {
813 crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
814 name,
815 ..
816 }) => {
817 assert_eq!(name, "test");
818 }
819 _ => panic!("Wrong error type"),
820 }
821 }
822
823 #[test]
824 fn test_register_duplicate_alias_fails() {
825 let mut registry = CommandRegistry::new();
826 let def1 = create_test_definition("cmd1", vec!["c"]);
827 let def2 = create_test_definition("cmd2", vec!["c"]);
828
829 registry.register_sync(def1, Box::new(TestHandler)).unwrap();
830 let result = registry.register_sync(def2, Box::new(TestHandler));
831
832 assert!(result.is_err());
833 match result.unwrap_err() {
834 crate::error::DynamicCliError::Registry(RegistryError::DuplicateAlias {
835 alias,
836 existing_command,
837 ..
838 }) => {
839 assert_eq!(alias, "c");
840 assert_eq!(existing_command, "cmd1");
841 }
842 _ => panic!("Wrong error type"),
843 }
844 }
845
846 #[test]
847 fn test_alias_conflicts_with_command_name() {
848 let mut registry = CommandRegistry::new();
849 let def1 = create_test_definition("test", vec![]);
850 let def2 = create_test_definition("other", vec!["test"]);
851
852 registry.register_sync(def1, Box::new(TestHandler)).unwrap();
853 let result = registry.register_sync(def2, Box::new(TestHandler));
854
855 assert!(result.is_err());
856 }
857
858 #[test]
859 fn test_command_name_conflicts_with_alias() {
860 let mut registry = CommandRegistry::new();
861 let def1 = create_test_definition("cmd1", vec!["other"]);
862 let def2 = create_test_definition("other", vec![]);
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 // Resolve name tests
871 #[test]
872 fn test_resolve_command_name() {
873 let mut registry = CommandRegistry::new();
874 let definition = create_test_definition("test", vec![]);
875
876 registry
877 .register_sync(definition, Box::new(TestHandler))
878 .unwrap();
879
880 assert_eq!(registry.resolve_name("test"), Some("test"));
881 }
882
883 #[test]
884 fn test_resolve_alias() {
885 let mut registry = CommandRegistry::new();
886 let definition = create_test_definition("hello", vec!["hi", "greet"]);
887
888 registry
889 .register_sync(definition, Box::new(TestHandler))
890 .unwrap();
891
892 assert_eq!(registry.resolve_name("hi"), Some("hello"));
893 assert_eq!(registry.resolve_name("greet"), Some("hello"));
894 }
895
896 #[test]
897 fn test_resolve_unknown_name() {
898 let registry = CommandRegistry::new();
899 assert_eq!(registry.resolve_name("unknown"), None);
900 }
901
902 // Get definition tests
903 #[test]
904 fn test_get_definition_by_name() {
905 let mut registry = CommandRegistry::new();
906 let definition = create_test_definition("test", vec![]);
907
908 registry
909 .register_sync(definition, Box::new(TestHandler))
910 .unwrap();
911
912 let retrieved = registry.get_definition("test");
913 assert!(retrieved.is_some());
914 assert_eq!(retrieved.unwrap().name, "test");
915 }
916
917 #[test]
918 fn test_get_definition_by_alias() {
919 let mut registry = CommandRegistry::new();
920 let definition = create_test_definition("hello", vec!["hi"]);
921
922 registry
923 .register_sync(definition, Box::new(TestHandler))
924 .unwrap();
925
926 let retrieved = registry.get_definition("hi");
927 assert!(retrieved.is_some());
928 assert_eq!(retrieved.unwrap().name, "hello");
929 }
930
931 #[test]
932 fn test_get_definition_unknown() {
933 let registry = CommandRegistry::new();
934 assert!(registry.get_definition("unknown").is_none());
935 }
936
937 // Get handler tests
938 #[test]
939 fn test_get_handler_by_name() {
940 let mut registry = CommandRegistry::new();
941 let definition = create_test_definition("test", vec![]);
942
943 registry
944 .register_sync(definition, Box::new(TestHandler))
945 .unwrap();
946
947 let handler = registry.get_handler_sync("test");
948 assert!(handler.is_some());
949 }
950
951 #[test]
952 fn test_get_handler_sync_by_name() {
953 let mut registry = CommandRegistry::new();
954 let definition = create_test_definition("test", vec![]);
955
956 registry
957 .register_sync(definition, Box::new(TestHandler))
958 .unwrap();
959
960 let handler = registry.get_handler_sync("test");
961 assert!(handler.is_some());
962 }
963
964 #[test]
965 fn test_get_handler_by_alias() {
966 let mut registry = CommandRegistry::new();
967 let definition = create_test_definition("hello", vec!["hi"]);
968
969 registry
970 .register_sync(definition, Box::new(TestHandler))
971 .unwrap();
972
973 let handler = registry.get_handler_sync("hi");
974 assert!(handler.is_some());
975 }
976
977 #[test]
978 fn test_get_handler_unknown() {
979 let registry = CommandRegistry::new();
980 assert!(registry.get_handler_sync("unknown").is_none());
981 }
982
983 // List commands tests
984 #[test]
985 fn test_list_commands_empty() {
986 let registry = CommandRegistry::new();
987 let commands = registry.list_commands();
988 assert_eq!(commands.len(), 0);
989 }
990
991 #[test]
992 fn test_list_commands_multiple() {
993 let mut registry = CommandRegistry::new();
994
995 registry
996 .register_sync(
997 create_test_definition("cmd1", vec![]),
998 Box::new(TestHandler),
999 )
1000 .unwrap();
1001 registry
1002 .register_sync(
1003 create_test_definition("cmd2", vec![]),
1004 Box::new(TestHandler),
1005 )
1006 .unwrap();
1007 registry
1008 .register_sync(
1009 create_test_definition("cmd3", vec![]),
1010 Box::new(TestHandler),
1011 )
1012 .unwrap();
1013
1014 let commands = registry.list_commands();
1015 assert_eq!(commands.len(), 3);
1016
1017 let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1018 assert!(names.contains(&"cmd1"));
1019 assert!(names.contains(&"cmd2"));
1020 assert!(names.contains(&"cmd3"));
1021 }
1022
1023 // Integration tests
1024 #[test]
1025 fn test_complete_workflow() {
1026 let mut registry = CommandRegistry::new();
1027
1028 // Register multiple commands with aliases
1029 let def1 = create_test_definition("simulate", vec!["sim", "run"]);
1030 let def2 = create_test_definition("validate", vec!["val", "check"]);
1031 let def3 = create_test_definition("help", vec!["h", "?"]);
1032
1033 registry.register_sync(def1, Box::new(TestHandler)).unwrap();
1034 registry.register_sync(def2, Box::new(TestHandler)).unwrap();
1035 registry.register_sync(def3, Box::new(TestHandler)).unwrap();
1036
1037 // Verify registry state
1038 assert_eq!(registry.len(), 3);
1039
1040 // Verify all names resolve correctly
1041 assert_eq!(registry.resolve_name("simulate"), Some("simulate"));
1042 assert_eq!(registry.resolve_name("sim"), Some("simulate"));
1043 assert_eq!(registry.resolve_name("validate"), Some("validate"));
1044 assert_eq!(registry.resolve_name("val"), Some("validate"));
1045
1046 // Verify handlers are accessible
1047 assert!(registry.get_handler_sync("simulate").is_some());
1048 assert!(registry.get_handler_sync("sim").is_some());
1049 assert!(registry.get_handler_sync("h").is_some());
1050
1051 // Verify definitions are accessible
1052 let sim_def = registry.get_definition("sim");
1053 assert!(sim_def.is_some());
1054 assert_eq!(sim_def.unwrap().name, "simulate");
1055 }
1056
1057 #[test]
1058 fn test_default_trait() {
1059 let registry: CommandRegistry = Default::default();
1060 assert!(registry.is_empty());
1061 }
1062
1063 #[test]
1064 fn test_contains_method() {
1065 let mut registry = CommandRegistry::new();
1066 let definition = create_test_definition("test", vec!["t"]);
1067
1068 registry
1069 .register_sync(definition, Box::new(TestHandler))
1070 .unwrap();
1071
1072 assert!(registry.contains("test"));
1073 assert!(registry.contains("t"));
1074 assert!(!registry.contains("unknown"));
1075 }
1076
1077 #[test]
1078 fn test_multiple_aliases_same_command() {
1079 let mut registry = CommandRegistry::new();
1080 let definition = create_test_definition("command", vec!["c", "cmd", "com"]);
1081
1082 registry
1083 .register_sync(definition, Box::new(TestHandler))
1084 .unwrap();
1085
1086 // All aliases should resolve to the same command
1087 assert_eq!(registry.resolve_name("c"), Some("command"));
1088 assert_eq!(registry.resolve_name("cmd"), Some("command"));
1089 assert_eq!(registry.resolve_name("com"), Some("command"));
1090
1091 // All should return the same handler
1092 let handler1 = registry.get_handler_sync("c");
1093 let handler2 = registry.get_handler_sync("cmd");
1094 assert!(handler1.is_some());
1095 assert!(handler2.is_some());
1096 }
1097
1098 #[test]
1099 fn test_case_sensitivity() {
1100 let mut registry = CommandRegistry::new();
1101 let definition = create_test_definition("Test", vec![]);
1102
1103 registry
1104 .register_sync(definition, Box::new(TestHandler))
1105 .unwrap();
1106
1107 // Case matters
1108 assert!(registry.contains("Test"));
1109 assert!(!registry.contains("test"));
1110 assert!(!registry.contains("TEST"));
1111 }
1112
1113 #[test]
1114 fn test_empty_alias_list() {
1115 let mut registry = CommandRegistry::new();
1116 let definition = create_test_definition("test", vec![]);
1117
1118 let result = registry.register_sync(definition, Box::new(TestHandler));
1119
1120 assert!(result.is_ok());
1121 assert!(registry.contains("test"));
1122 }
1123
1124 // ============================================================================
1125 // AsyncCommandHandler / register_async / get_handler_async TESTS (DD-022)
1126 // ============================================================================
1127
1128 #[test]
1129 fn test_register_async_command() {
1130 let mut registry = CommandRegistry::new();
1131 let definition = create_test_definition("fetch", vec![]);
1132
1133 let result = registry.register_async(definition, Box::new(TestAsyncHandler));
1134
1135 assert!(result.is_ok());
1136 assert_eq!(registry.len(), 1);
1137 }
1138
1139 #[test]
1140 fn test_register_async_command_with_aliases() {
1141 let mut registry = CommandRegistry::new();
1142 let definition = create_test_definition("fetch", vec!["f", "get-remote"]);
1143
1144 registry
1145 .register_async(definition, Box::new(TestAsyncHandler))
1146 .unwrap();
1147
1148 assert!(registry.contains("fetch"));
1149 assert!(registry.contains("f"));
1150 assert!(registry.contains("get-remote"));
1151 assert_eq!(registry.resolve_name("f"), Some("fetch"));
1152 }
1153
1154 #[test]
1155 fn test_get_handler_async_by_name_and_alias() {
1156 let mut registry = CommandRegistry::new();
1157 let definition = create_test_definition("fetch", vec!["f"]);
1158
1159 registry
1160 .register_async(definition, Box::new(TestAsyncHandler))
1161 .unwrap();
1162
1163 assert!(registry.get_handler_async("fetch").is_some());
1164 assert!(registry.get_handler_async("f").is_some());
1165 assert!(registry.get_handler_async("unknown").is_none());
1166 }
1167
1168 /// The core cross-accessor guarantee DD-022 depends on: querying an
1169 /// async-registered command through the *sync* accessor returns `None`
1170 /// (not the wrong handler, not a panic) — dispatch sites rely on this
1171 /// to fall through from `get_handler_sync` to `get_handler_async`.
1172 #[test]
1173 fn test_sync_accessor_returns_none_for_async_command() {
1174 let mut registry = CommandRegistry::new();
1175 let definition = create_test_definition("fetch", vec![]);
1176
1177 registry
1178 .register_async(definition, Box::new(TestAsyncHandler))
1179 .unwrap();
1180
1181 assert!(registry.get_handler_sync("fetch").is_none());
1182 assert!(registry.get_handler_async("fetch").is_some());
1183 }
1184
1185 /// Symmetric case: querying a sync-registered command through the
1186 /// *async* accessor returns `None`.
1187 #[test]
1188 fn test_async_accessor_returns_none_for_sync_command() {
1189 let mut registry = CommandRegistry::new();
1190 let definition = create_test_definition("test", vec![]);
1191
1192 registry
1193 .register_sync(definition, Box::new(TestHandler))
1194 .unwrap();
1195
1196 assert!(registry.get_handler_async("test").is_none());
1197 assert!(registry.get_handler_sync("test").is_some());
1198 }
1199
1200 /// A command name already taken by a sync handler must be rejected for
1201 /// async registration — the unified storage means one name, one kind.
1202 #[test]
1203 fn test_register_async_conflicts_with_existing_sync_name() {
1204 let mut registry = CommandRegistry::new();
1205 let sync_def = create_test_definition("dual", vec![]);
1206 let async_def = create_test_definition("dual", vec![]);
1207
1208 registry
1209 .register_sync(sync_def, Box::new(TestHandler))
1210 .unwrap();
1211 let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1212
1213 assert!(result.is_err());
1214 match result.unwrap_err() {
1215 crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
1216 name,
1217 ..
1218 }) => {
1219 assert_eq!(name, "dual");
1220 }
1221 other => panic!("Expected DuplicateRegistration, got: {:?}", other),
1222 }
1223 }
1224
1225 /// Symmetric case: a name already taken by an async handler must be
1226 /// rejected for sync registration.
1227 #[test]
1228 fn test_register_sync_conflicts_with_existing_async_name() {
1229 let mut registry = CommandRegistry::new();
1230 let async_def = create_test_definition("dual", vec![]);
1231 let sync_def = create_test_definition("dual", vec![]);
1232
1233 registry
1234 .register_async(async_def, Box::new(TestAsyncHandler))
1235 .unwrap();
1236 let result = registry.register_sync(sync_def, Box::new(TestHandler));
1237
1238 assert!(result.is_err());
1239 }
1240
1241 /// An async command's alias must not collide with an existing sync
1242 /// command's alias, and vice versa — conflict detection is shared
1243 /// across both kinds via `check_name_available`.
1244 #[test]
1245 fn test_async_alias_conflicts_with_sync_alias() {
1246 let mut registry = CommandRegistry::new();
1247 let sync_def = create_test_definition("cmd1", vec!["shared"]);
1248 let async_def = create_test_definition("cmd2", vec!["shared"]);
1249
1250 registry
1251 .register_sync(sync_def, Box::new(TestHandler))
1252 .unwrap();
1253 let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1254
1255 assert!(result.is_err());
1256 }
1257
1258 #[test]
1259 fn test_get_definition_works_for_async_command() {
1260 let mut registry = CommandRegistry::new();
1261 let definition = create_test_definition("fetch", vec!["f"]);
1262
1263 registry
1264 .register_async(definition, Box::new(TestAsyncHandler))
1265 .unwrap();
1266
1267 let retrieved = registry.get_definition("f");
1268 assert!(retrieved.is_some());
1269 assert_eq!(retrieved.unwrap().name, "fetch");
1270 }
1271
1272 #[test]
1273 fn test_list_commands_includes_both_sync_and_async() {
1274 let mut registry = CommandRegistry::new();
1275
1276 registry
1277 .register_sync(
1278 create_test_definition("sync-cmd", vec![]),
1279 Box::new(TestHandler),
1280 )
1281 .unwrap();
1282 registry
1283 .register_async(
1284 create_test_definition("async-cmd", vec![]),
1285 Box::new(TestAsyncHandler),
1286 )
1287 .unwrap();
1288
1289 let commands = registry.list_commands();
1290 assert_eq!(commands.len(), 2);
1291 let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1292 assert!(names.contains(&"sync-cmd"));
1293 assert!(names.contains(&"async-cmd"));
1294 }
1295
1296 #[test]
1297 fn test_mixed_registry_workflow() {
1298 // End-to-end: a registry with both sync and async commands behaves
1299 // consistently across resolve_name / get_definition / len / contains.
1300 let mut registry = CommandRegistry::new();
1301
1302 registry
1303 .register_sync(
1304 create_test_definition("simulate", vec!["sim"]),
1305 Box::new(TestHandler),
1306 )
1307 .unwrap();
1308 registry
1309 .register_async(
1310 create_test_definition("fetch", vec!["f"]),
1311 Box::new(TestAsyncHandler),
1312 )
1313 .unwrap();
1314
1315 assert_eq!(registry.len(), 2);
1316 assert!(registry.contains("sim"));
1317 assert!(registry.contains("f"));
1318
1319 assert!(registry.get_handler_sync("simulate").is_some());
1320 assert!(registry.get_handler_async("fetch").is_some());
1321 assert!(registry.get_handler_sync("fetch").is_none());
1322 assert!(registry.get_handler_async("simulate").is_none());
1323 }
1324}