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