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