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