dynamic_cli/executor/traits.rs
1//! Command handler trait and related types
2//!
3//! This module defines the core trait that all command implementations must implement.
4//! The trait is designed to be object-safe, meaning it can be used as a trait object
5//! (`&dyn CommandHandler`), which is critical for dynamic command registration.
6//!
7//! # Design Principles
8//!
9//! ## Object Safety
10//!
11//! The `CommandHandler` trait is intentionally kept simple and object-safe:
12//! - No generic methods (would prevent trait object usage)
13//! - No associated types with type parameters
14//! - All methods use concrete types or trait objects
15//!
16//! This allows the registry to store handlers as `Box<dyn CommandHandler>`,
17//! enabling dynamic command registration at runtime.
18//!
19//! ## Simple Type Signatures
20//!
21//! Arguments are passed as [`crate::parser::ParsedArgs`] rather than generic
22//! types. This design choice:
23//! - Maintains object safety
24//! - Represents both scalar and repeatable-option values (DD-024)
25//! - Delegates type parsing to the parser module
26//!
27//! ## Thread Safety
28//!
29//! All handlers must be `Send + Sync` to support:
30//! - Shared access across threads
31//! - Potential async execution in the future
32//! - Safe usage in multi-threaded contexts
33//!
34//! # Example
35//!
36//! ```
37//! use dynamic_cli::executor::{CommandHandler, ParsedArgs};
38//! use dynamic_cli::context::ExecutionContext;
39//! use dynamic_cli::Result;
40//!
41//! // Define a simple command handler
42//! struct HelloCommand;
43//!
44//! impl CommandHandler for HelloCommand {
45//! fn execute(
46//! &self,
47//! _context: &mut dyn ExecutionContext,
48//! args: &ParsedArgs,
49//! ) -> Result<()> {
50//! let name = args.get_scalar("name").unwrap_or("World");
51//! println!("Hello, {}!", name);
52//! Ok(())
53//! }
54//! }
55//! ```
56
57use crate::context::ExecutionContext;
58use crate::error::Result;
59use crate::parser::ParsedArgs;
60use async_trait::async_trait;
61
62/// Trait for command implementations
63///
64/// Each command in the CLI/REPL application must implement this trait.
65/// The trait is designed to be object-safe, allowing commands to be
66/// stored and invoked dynamically through trait objects.
67///
68/// # Object Safety
69///
70/// This trait is intentionally object-safe (can be used as `dyn CommandHandler`).
71/// **Do not add methods with generic type parameters**, as this would break
72/// object safety and prevent dynamic dispatch.
73///
74/// # Thread Safety
75///
76/// Implementations must be `Send + Sync` to allow:
77/// - Sharing command handlers across threads
78/// - Safe concurrent access to the command registry
79/// - Future async execution support
80///
81/// # Execution Flow
82///
83/// 0. `expected_fault_tolerance()` is checked once, at registration time
84/// (DD-028) — before any of the steps below, and before this handler
85/// ever sees a command line.
86/// 1. Parser converts user input to [`crate::parser::ParsedArgs`]
87/// 2. Validator checks argument constraints
88/// 3. `validate()` is called for custom validation (optional)
89/// 4. `execute()` is called with validated arguments
90///
91/// # Example
92///
93/// ```
94/// use dynamic_cli::error::ExecutionError;
95/// use dynamic_cli::executor::{CommandHandler, ParsedArgs};
96/// use dynamic_cli::context::ExecutionContext;
97/// use dynamic_cli::Result;
98///
99/// struct GreetCommand;
100///
101/// impl CommandHandler for GreetCommand {
102/// fn execute(
103/// &self,
104/// _context: &mut dyn ExecutionContext,
105/// args: &ParsedArgs,
106/// ) -> Result<()> {
107/// let name = args.get_scalar("name")
108/// .ok_or_else(|| {
109/// ExecutionError::CommandFailed(
110/// anyhow::anyhow!("Missing 'name' argument")
111/// )
112/// })?;
113///
114/// let greeting = if let Some(formal) = args.get_scalar("formal") {
115/// if formal == "true" {
116/// format!("Good day, {}.", name)
117/// } else {
118/// format!("Hi, {}!", name)
119/// }
120/// } else {
121/// format!("Hello, {}!", name)
122/// };
123///
124/// println!("{}", greeting);
125/// Ok(())
126/// }
127///
128/// fn validate(&self, args: &ParsedArgs) -> Result<()> {
129/// // Custom validation: name must not be empty
130/// if let Some(name) = args.get_scalar("name") {
131/// if name.trim().is_empty() {
132/// return Err(ExecutionError::CommandFailed(
133/// anyhow::anyhow!("Name cannot be empty")
134/// ).into());
135/// }
136/// }
137/// Ok(())
138/// }
139/// }
140/// ```
141pub trait CommandHandler: Send + Sync {
142 /// Execute the command with the given context and arguments
143 ///
144 /// This is the main entry point for command execution. It receives:
145 /// - A mutable reference to the execution context (for shared state)
146 /// - A map of argument names to their string values
147 ///
148 /// # Arguments
149 ///
150 /// * `context` - Mutable execution context for sharing state between commands.
151 /// Use `downcast_ref` or `downcast_mut` from the `context` module
152 /// to access your specific context type.
153 ///
154 /// * `args` - Parsed and validated arguments as name-value pairs.
155 /// All values are strings; type conversion should be done
156 /// within the handler if needed.
157 ///
158 /// # Returns
159 ///
160 /// - `Ok(())` if execution succeeds
161 /// - `Err(DynamicCliError)` if execution fails
162 ///
163 /// # Errors
164 ///
165 /// Implementations should return errors for:
166 /// - Invalid argument values (caught by validate, but can be rechecked)
167 /// - Execution failures (I/O errors, computation errors, etc.)
168 /// - Invalid context state
169 ///
170 /// Use `ExecutionError::CommandFailed` to wrap application-specific errors:
171 /// ```ignore
172 /// Err(ExecutionError::CommandFailed(anyhow::anyhow!("Details")).into())
173 /// ```
174 ///
175 /// # Example
176 ///
177 /// ```
178 /// # use dynamic_cli::error::ExecutionError;
179 /// # use dynamic_cli::executor::{CommandHandler, ParsedArgs};
180 /// # use dynamic_cli::context::ExecutionContext;
181 /// # use dynamic_cli::Result;
182 /// #
183 /// struct FileCommand;
184 ///
185 /// impl CommandHandler for FileCommand {
186 /// fn execute(
187 /// &self,
188 /// _context: &mut dyn ExecutionContext,
189 /// args: &ParsedArgs,
190 /// ) -> Result<()> {
191 /// let path = args.get_scalar("path")
192 /// .ok_or_else(|| {
193 /// ExecutionError::CommandFailed(
194 /// anyhow::anyhow!("Missing path argument")
195 /// )
196 /// })?;
197 ///
198 /// // Perform the actual work
199 /// let content = std::fs::read_to_string(path)
200 /// .map_err(|e| {
201 /// ExecutionError::CommandFailed(anyhow::anyhow!("Failed to read file: {}", e))
202 /// })?;
203 ///
204 /// println!("File contains {} bytes", content.len());
205 /// Ok(())
206 /// }
207 /// }
208 /// ```
209 fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()>;
210
211 /// Optional custom validation for arguments
212 ///
213 /// This method is called after the standard validation (type checking,
214 /// required arguments, etc.) but before execution. It allows commands
215 /// to implement custom validation logic.
216 ///
217 /// # Default Implementation
218 ///
219 /// The default implementation accepts all arguments (returns `Ok(())`).
220 /// Override this method only if you need custom validation.
221 ///
222 /// # Arguments
223 ///
224 /// * `args` - The arguments to validate
225 ///
226 /// # Returns
227 ///
228 /// - `Ok(())` if validation succeeds
229 /// - `Err(DynamicCliError)` if validation fails
230 ///
231 /// # Example
232 ///
233 /// ```
234 /// # use dynamic_cli::executor::{CommandHandler, ParsedArgs};
235 /// # use dynamic_cli::context::ExecutionContext;
236 /// # use dynamic_cli::error::ExecutionError;
237 /// # use dynamic_cli::Result;
238 /// #
239 /// struct RangeCommand;
240 ///
241 /// impl CommandHandler for RangeCommand {
242 /// fn execute(
243 /// &self,
244 /// _context: &mut dyn ExecutionContext,
245 /// args: &ParsedArgs,
246 /// ) -> Result<()> {
247 /// // Execution logic here
248 /// Ok(())
249 /// }
250 ///
251 /// fn validate(&self, args: &ParsedArgs) -> Result<()> {
252 /// // Custom validation: ensure min < max
253 /// if let (Some(min), Some(max)) = (args.get_scalar("min"), args.get_scalar("max")) {
254 /// let min_val: f64 = min.parse()
255 /// .map_err(|_| {
256 /// ExecutionError::CommandFailed(anyhow::anyhow!("Invalid min value"))
257 /// })?;
258 /// let max_val: f64 = max.parse()
259 /// .map_err(|_| {ExecutionError::CommandFailed(anyhow::anyhow!("Invalid max value"))})?;
260 ///
261 /// if min_val >= max_val {
262 /// return Err(ExecutionError::CommandFailed(anyhow::anyhow!("min must be less than max")).into());
263 /// }
264 /// }
265 /// Ok(())
266 /// }
267 /// }
268 /// ```
269 fn validate(&self, _args: &ParsedArgs) -> Result<()> {
270 Ok(())
271 }
272
273 /// Declare an expectation about this command's configured
274 /// `continue_on_failure` (DD-026), if the handler's author has one.
275 ///
276 /// Purely informational at the trait level: `CommandRegistry::register`
277 /// compares this value (when `Some`) against the `continue_on_failure`
278 /// the YAML config actually declares for this command, and rejects
279 /// registration on a contradiction (`RegistryError::FaultToleranceMismatch`,
280 /// DD-028) — catching a misconfiguration at startup instead of at first
281 /// use. It does not, and cannot, verify that the handler's own code
282 /// actually behaves as declared; that remains the author's
283 /// responsibility, covered by the application's own tests.
284 ///
285 /// # Default Implementation
286 ///
287 /// `None` — no opinion, no check performed. Override only if this
288 /// handler's `execute()` was written under a specific assumption about
289 /// whether the chain may safely continue past its failure.
290 ///
291 /// # Example
292 ///
293 /// ```
294 /// # use dynamic_cli::executor::{CommandHandler, ParsedArgs};
295 /// # use dynamic_cli::context::ExecutionContext;
296 /// # use dynamic_cli::Result;
297 /// #
298 /// /// A command whose failure must always stop the chain — the handler
299 /// /// author never wants this one silently skipped over.
300 /// struct SolveCommand;
301 ///
302 /// impl CommandHandler for SolveCommand {
303 /// fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
304 /// Ok(())
305 /// }
306 ///
307 /// fn expected_fault_tolerance(&self) -> Option<bool> {
308 /// Some(false)
309 /// }
310 /// }
311 /// ```
312 fn expected_fault_tolerance(&self) -> Option<bool> {
313 None
314 }
315}
316
317/// Async counterpart of [`CommandHandler`].
318///
319/// Additive to `CommandHandler` (see DD-022) — it does not replace it.
320/// Implementations use this trait when their command body needs to perform
321/// async I/O (network calls, streaming, etc.). The signatures deliberately
322/// mirror `CommandHandler` exactly, `execute`/`validate` aside from the
323/// `async` keyword, so that migrating a handler from sync to async is a
324/// mechanical change.
325///
326/// # Object Safety
327///
328/// Made `dyn`-compatible via `#[async_trait]` (which desugars `async fn` to
329/// a boxed, pinned future under the hood). Stored as `Box<dyn
330/// AsyncCommandHandler>` in the registry, exactly like `CommandHandler` is
331/// stored as `Box<dyn CommandHandler>`.
332///
333/// # Thread Safety
334///
335/// Same constraint as `CommandHandler`: `Send + Sync` is required so the
336/// handler can be shared across the registry and, transitively, across
337/// threads if the application needs it.
338///
339/// # Why a separate trait instead of an async `CommandHandler`?
340///
341/// Existing sync `CommandHandler` implementations (including downstream
342/// consumers) must keep compiling unchanged. See DD-022 for the full
343/// rationale, including why `tokio` is not a dependency of `dynamic-cli`
344/// itself and why driving the returned future via
345/// `futures::executor::block_on` at the dispatch site is safe.
346///
347/// # Example
348///
349/// ```
350/// use async_trait::async_trait;
351/// use dynamic_cli::executor::{AsyncCommandHandler, ParsedArgs};
352/// use dynamic_cli::context::ExecutionContext;
353/// use dynamic_cli::error::ExecutionError;
354/// use dynamic_cli::Result;
355///
356/// struct FetchCommand;
357///
358/// #[async_trait]
359/// impl AsyncCommandHandler for FetchCommand {
360/// async fn execute(
361/// &self,
362/// _context: &mut dyn ExecutionContext,
363/// args: &ParsedArgs,
364/// ) -> Result<()> {
365/// let url = args.get_scalar("url").ok_or_else(|| {
366/// ExecutionError::CommandFailed(anyhow::anyhow!("Missing 'url' argument"))
367/// })?;
368/// // Real implementations would `.await` an async HTTP call here.
369/// println!("Fetching {url}...");
370/// Ok(())
371/// }
372///
373/// async fn validate(&self, args: &ParsedArgs) -> Result<()> {
374/// if args.get_scalar("url").is_none() {
375/// return Err(ExecutionError::CommandFailed(anyhow::anyhow!("url is required")).into());
376/// }
377/// Ok(())
378/// }
379/// }
380/// ```
381#[async_trait]
382pub trait AsyncCommandHandler: Send + Sync {
383 /// Async equivalent of [`CommandHandler::execute`]. Same contract:
384 /// receives the mutable execution context and the parsed arguments,
385 /// returns `Ok(())` on success or a `DynamicCliError` on failure.
386 async fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()>;
387
388 /// Async equivalent of [`CommandHandler::validate`]. Same contract and
389 /// same default (accepts all arguments) — override only for custom
390 /// validation logic.
391 async fn validate(&self, _args: &ParsedArgs) -> Result<()> {
392 Ok(())
393 }
394
395 /// Mirrors [`CommandHandler::expected_fault_tolerance`] exactly — same
396 /// contract, same default (`None`), checked by
397 /// `CommandRegistry::register_async` the same way `register_sync` checks
398 /// it for [`CommandHandler`] (DD-028).
399 ///
400 /// Deliberately **not** an `async fn`, unlike `execute`/`validate`
401 /// above: this method does no work of its own and is evaluated at
402 /// registration time, before any dispatch — sync or async — so making
403 /// it async would only force `register_async` (itself a synchronous
404 /// function) to `block_on` it for no benefit.
405 fn expected_fault_tolerance(&self) -> Option<bool> {
406 None
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use crate::error::ExecutionError;
414 use std::any::Any;
415 use std::collections::HashMap;
416 use std::sync::Arc;
417
418 /// Test helper: build a scalar-only `ParsedArgs` from `[(key, value), ...]`.
419 fn scalar_args<const N: usize>(pairs: [(&str, &str); N]) -> ParsedArgs {
420 let map: HashMap<String, String> = pairs
421 .into_iter()
422 .map(|(k, v)| (k.to_string(), v.to_string()))
423 .collect();
424 ParsedArgs::from_scalars(map)
425 }
426
427 // ============================================================================
428 // TEST FIXTURES
429 // ============================================================================
430
431 /// Simple test context for unit tests
432 #[derive(Default)]
433 struct TestContext {
434 state: String,
435 }
436
437 impl ExecutionContext for TestContext {
438 fn as_any(&self) -> &dyn Any {
439 self
440 }
441
442 fn as_any_mut(&mut self) -> &mut dyn Any {
443 self
444 }
445 }
446
447 /// Simple command that prints to context
448 struct HelloCommand;
449
450 impl CommandHandler for HelloCommand {
451 fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()> {
452 let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
453 ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
454 })?;
455
456 let name = args.get_scalar("name").unwrap_or("World");
457 ctx.state = format!("Hello, {}!", name);
458 Ok(())
459 }
460 }
461
462 /// Command with custom validation
463 struct ValidatedCommand;
464
465 impl CommandHandler for ValidatedCommand {
466 fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
467 Ok(())
468 }
469
470 fn validate(&self, args: &ParsedArgs) -> Result<()> {
471 // Require "count" argument to be present and > 0
472 if let Some(count) = args.get_scalar("count") {
473 let count_val: i32 = count.parse().map_err(|_| {
474 ExecutionError::CommandFailed(anyhow::anyhow!("count must be an integer"))
475 })?;
476
477 if count_val <= 0 {
478 return Err(ExecutionError::CommandFailed(anyhow::anyhow!(
479 "count must be positive"
480 ))
481 .into());
482 }
483 } else {
484 return Err(
485 ExecutionError::CommandFailed(anyhow::anyhow!("count is required")).into(),
486 );
487 }
488 Ok(())
489 }
490 }
491
492 /// Command that fails during execution
493 struct FailingCommand;
494
495 impl CommandHandler for FailingCommand {
496 fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
497 Err(ExecutionError::CommandFailed(anyhow::anyhow!("Simulated failure")).into())
498 }
499 }
500
501 /// Command that modifies context
502 struct StatefulCommand;
503
504 impl CommandHandler for StatefulCommand {
505 fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()> {
506 let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
507 ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
508 })?;
509
510 let value = args.get_scalar("value").unwrap_or("default");
511 ctx.state.push_str(value);
512 Ok(())
513 }
514 }
515
516 // ============================================================================
517 // BASIC FUNCTIONALITY TESTS
518 // ============================================================================
519
520 #[test]
521 fn test_basic_execution() {
522 let handler = HelloCommand;
523 let mut context = TestContext::default();
524 let args = scalar_args([("name", "Rust")]);
525
526 let result = handler.execute(&mut context, &args);
527
528 assert!(result.is_ok());
529 assert_eq!(context.state, "Hello, Rust!");
530 }
531
532 #[test]
533 fn test_execution_without_args() {
534 let handler = HelloCommand;
535 let mut context = TestContext::default();
536 let args = ParsedArgs::from_scalars(HashMap::new());
537
538 let result = handler.execute(&mut context, &args);
539
540 assert!(result.is_ok());
541 assert_eq!(context.state, "Hello, World!");
542 }
543
544 #[test]
545 fn test_execution_with_empty_name() {
546 let handler = HelloCommand;
547 let mut context = TestContext::default();
548 let args = scalar_args([("name", "")]);
549
550 let result = handler.execute(&mut context, &args);
551
552 assert!(result.is_ok());
553 assert_eq!(context.state, "Hello, !");
554 }
555
556 // ============================================================================
557 // VALIDATION TESTS
558 // ============================================================================
559
560 #[test]
561 fn test_default_validation_accepts_all() {
562 let handler = HelloCommand;
563 let args = scalar_args([("random", "value")]);
564
565 let result = handler.validate(&args);
566
567 assert!(result.is_ok());
568 }
569
570 #[test]
571 fn test_custom_validation_success() {
572 let handler = ValidatedCommand;
573 let args = scalar_args([("count", "5")]);
574
575 let result = handler.validate(&args);
576
577 assert!(result.is_ok());
578 }
579
580 #[test]
581 fn test_custom_validation_missing_arg() {
582 let handler = ValidatedCommand;
583 let args = ParsedArgs::from_scalars(HashMap::new());
584
585 let result = handler.validate(&args);
586
587 assert!(result.is_err());
588 let err_msg = format!("{}", result.unwrap_err());
589 assert!(err_msg.contains("required"));
590 }
591
592 #[test]
593 fn test_custom_validation_invalid_value() {
594 let handler = ValidatedCommand;
595 let args = scalar_args([("count", "0")]);
596
597 let result = handler.validate(&args);
598
599 assert!(result.is_err());
600 let err_msg = format!("{}", result.unwrap_err());
601 assert!(err_msg.contains("positive"));
602 }
603
604 #[test]
605 fn test_custom_validation_non_integer() {
606 let handler = ValidatedCommand;
607 let args = scalar_args([("count", "abc")]);
608
609 let result = handler.validate(&args);
610
611 assert!(result.is_err());
612 let err_msg = format!("{}", result.unwrap_err());
613 assert!(err_msg.contains("integer"));
614 }
615
616 // ============================================================================
617 // ERROR HANDLING TESTS
618 // ============================================================================
619
620 #[test]
621 fn test_execution_failure() {
622 let handler = FailingCommand;
623 let mut context = TestContext::default();
624 let args = ParsedArgs::from_scalars(HashMap::new());
625
626 let result = handler.execute(&mut context, &args);
627
628 assert!(result.is_err());
629 let err_msg = format!("{}", result.unwrap_err());
630 assert!(err_msg.contains("Simulated failure"));
631 }
632
633 #[test]
634 fn test_context_downcast_failure() {
635 // Use a different context type to trigger downcast failure
636 struct WrongContext;
637
638 impl ExecutionContext for WrongContext {
639 fn as_any(&self) -> &dyn Any {
640 self
641 }
642
643 fn as_any_mut(&mut self) -> &mut dyn Any {
644 self
645 }
646 }
647
648 let handler = HelloCommand;
649 let mut wrong_context = WrongContext;
650 let args = ParsedArgs::from_scalars(HashMap::new());
651
652 let result = handler.execute(&mut wrong_context, &args);
653
654 assert!(result.is_err());
655 let err_msg = format!("{}", result.unwrap_err());
656 assert!(err_msg.contains("Wrong context type"));
657 }
658
659 // ============================================================================
660 // STATE MODIFICATION TESTS
661 // ============================================================================
662
663 #[test]
664 fn test_context_state_modification() {
665 let handler = StatefulCommand;
666 let mut context = TestContext {
667 state: "initial".to_string(),
668 };
669 let args = scalar_args([("value", "_modified")]);
670
671 let result = handler.execute(&mut context, &args);
672
673 assert!(result.is_ok());
674 assert_eq!(context.state, "initial_modified");
675 }
676
677 #[test]
678 fn test_multiple_executions_preserve_state() {
679 let handler = StatefulCommand;
680 let mut context = TestContext::default();
681
682 // First execution
683 let args1 = scalar_args([("value", "first")]);
684 handler.execute(&mut context, &args1).unwrap();
685 assert_eq!(context.state, "first");
686
687 // Second execution
688 let args2 = scalar_args([("value", "_second")]);
689 handler.execute(&mut context, &args2).unwrap();
690 assert_eq!(context.state, "first_second");
691 }
692
693 // ============================================================================
694 // TRAIT OBJECT TESTS
695 // ============================================================================
696
697 #[test]
698 fn test_trait_object_usage() {
699 // Verify that CommandHandler can be used as a trait object
700 let handler: Box<dyn CommandHandler> = Box::new(HelloCommand);
701 let mut context = TestContext::default();
702 let args = scalar_args([("name", "TraitObject")]);
703
704 let result = handler.execute(&mut context, &args);
705
706 assert!(result.is_ok());
707 assert_eq!(context.state, "Hello, TraitObject!");
708 }
709
710 #[test]
711 fn test_multiple_trait_objects() {
712 // Store multiple handlers as trait objects
713 let handlers: Vec<Box<dyn CommandHandler>> =
714 vec![Box::new(HelloCommand), Box::new(StatefulCommand)];
715
716 let mut context = TestContext::default();
717
718 // Execute first handler
719 let args1 = scalar_args([("name", "First")]);
720 handlers[0].execute(&mut context, &args1).unwrap();
721 assert_eq!(context.state, "Hello, First!");
722
723 // Execute second handler
724 context.state.clear();
725 let args2 = scalar_args([("value", "Second")]);
726 handlers[1].execute(&mut context, &args2).unwrap();
727 assert_eq!(context.state, "Second");
728 }
729
730 // ============================================================================
731 // THREAD SAFETY TESTS
732 // ============================================================================
733
734 #[test]
735 fn test_send_sync_requirement() {
736 // This test verifies that CommandHandler is Send + Sync
737 // by using it in a multi-threaded context
738 let handler: Arc<dyn CommandHandler> = Arc::new(HelloCommand);
739
740 // Clone the Arc to simulate sharing across threads
741 let handler_clone = handler.clone();
742
743 // This compilation test ensures Send + Sync are satisfied
744 let _ = std::thread::spawn(move || {
745 let _h = handler_clone;
746 });
747 }
748
749 #[test]
750 fn test_concurrent_validation() {
751 // Test that validation can be called from multiple threads
752 let handler = Arc::new(ValidatedCommand);
753 let handler_clone = handler.clone();
754
755 let handle = std::thread::spawn(move || {
756 let args = scalar_args([("count", "10")]);
757 handler_clone.validate(&args)
758 });
759
760 let args = scalar_args([("count", "5")]);
761 let result1 = handler.validate(&args);
762
763 let result2 = handle.join().unwrap();
764
765 assert!(result1.is_ok());
766 assert!(result2.is_ok());
767 }
768
769 // ============================================================================
770 // EDGE CASES
771 // ============================================================================
772
773 #[test]
774 fn test_empty_args() {
775 let handler = StatefulCommand;
776 let mut context = TestContext::default();
777 let args = ParsedArgs::from_scalars(HashMap::new());
778
779 // Should use default value
780 let result = handler.execute(&mut context, &args);
781
782 assert!(result.is_ok());
783 assert_eq!(context.state, "default");
784 }
785
786 #[test]
787 fn test_args_with_special_characters() {
788 let handler = HelloCommand;
789 let mut context = TestContext::default();
790 let args = scalar_args([("name", "Hello, 世界! 🌍")]);
791
792 let result = handler.execute(&mut context, &args);
793
794 assert!(result.is_ok());
795 assert_eq!(context.state, "Hello, Hello, 世界! 🌍!");
796 }
797
798 #[test]
799 fn test_very_long_argument() {
800 let handler = HelloCommand;
801 let mut context = TestContext::default();
802 let long_name = "x".repeat(10000);
803 let args = scalar_args([("name", long_name.as_str())]);
804
805 let result = handler.execute(&mut context, &args);
806
807 assert!(result.is_ok());
808 assert!(context.state.contains(&long_name));
809 }
810
811 // ============================================================================
812 // SHARED STATE TESTS
813 // ============================================================================
814
815 #[test]
816 fn test_shared_mutable_context() {
817 // Test that context can be safely modified by multiple commands
818 let handler1 = StatefulCommand;
819 let handler2 = StatefulCommand;
820 let mut context = TestContext::default();
821
822 let args1 = scalar_args([("value", "A")]);
823 handler1.execute(&mut context, &args1).unwrap();
824
825 let args2 = scalar_args([("value", "B")]);
826 handler2.execute(&mut context, &args2).unwrap();
827
828 assert_eq!(context.state, "AB");
829 }
830
831 // Test to ensure the trait is indeed object-safe at compile time
832 #[test]
833 fn test_object_safety_compile_time() {
834 // This function signature requires CommandHandler to be object-safe
835 fn _accepts_trait_object(_: &dyn CommandHandler) {}
836
837 // If this compiles, the trait is object-safe
838 let handler = HelloCommand;
839 _accepts_trait_object(&handler);
840 }
841
842 // Test that demonstrates why we can't have generic methods
843 // (This is a documentation test, not an actual test that runs)
844 /// ```compile_fail
845 /// use dynamic_cli::executor::CommandHandler;
846 ///
847 /// trait BrokenHandler: CommandHandler {
848 /// fn generic_method<T>(&self, value: T);
849 /// }
850 ///
851 /// // This would fail because trait objects can't have generic methods
852 /// fn use_as_trait_object(handler: &dyn BrokenHandler) {
853 /// // Cannot call generic_method on trait object
854 /// }
855 /// ```
856 #[allow(dead_code)]
857 fn test_no_generic_methods_documentation() {}
858
859 // ============================================================================
860 // expected_fault_tolerance() TESTS (DD-028)
861 // ============================================================================
862
863 /// A handler whose author never expressed an opinion — the common case.
864 struct FaultAgnosticCommand;
865
866 impl CommandHandler for FaultAgnosticCommand {
867 fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
868 Ok(())
869 }
870 }
871
872 /// A handler whose author declares its failures must never be ignored.
873 struct FaultIntolerantCommand;
874
875 impl CommandHandler for FaultIntolerantCommand {
876 fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
877 Ok(())
878 }
879
880 fn expected_fault_tolerance(&self) -> Option<bool> {
881 Some(false)
882 }
883 }
884
885 /// A handler whose author declares its failures are safe to ignore.
886 struct FaultTolerantCommand;
887
888 impl CommandHandler for FaultTolerantCommand {
889 fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
890 Ok(())
891 }
892
893 fn expected_fault_tolerance(&self) -> Option<bool> {
894 Some(true)
895 }
896 }
897
898 #[test]
899 fn test_expected_fault_tolerance_defaults_to_none() {
900 // Every pre-existing handler that never heard of DD-028 keeps
901 // compiling and behaving exactly as before — no supertrait, no
902 // required impl, nothing to add.
903 let handler = FaultAgnosticCommand;
904 assert_eq!(handler.expected_fault_tolerance(), None);
905
906 // HelloCommand and ValidatedCommand, defined above DD-028 existed,
907 // are just as unaffected.
908 assert_eq!(HelloCommand.expected_fault_tolerance(), None);
909 assert_eq!(ValidatedCommand.expected_fault_tolerance(), None);
910 }
911
912 #[test]
913 fn test_expected_fault_tolerance_override() {
914 assert_eq!(
915 FaultIntolerantCommand.expected_fault_tolerance(),
916 Some(false)
917 );
918 assert_eq!(FaultTolerantCommand.expected_fault_tolerance(), Some(true));
919 }
920
921 #[test]
922 fn test_expected_fault_tolerance_via_trait_object() {
923 // The method must be reachable through `Box<dyn CommandHandler>`,
924 // exactly like `execute`/`validate` — this is what
925 // `CommandRegistry::register_sync` actually calls it through.
926 let handler: Box<dyn CommandHandler> = Box::new(FaultIntolerantCommand);
927 assert_eq!(handler.expected_fault_tolerance(), Some(false));
928 }
929
930 // ============================================================================
931 // AsyncCommandHandler TESTS (DD-022)
932 // ============================================================================
933
934 /// Async command that writes to the test context, mirroring `HelloCommand`.
935 struct AsyncHelloCommand;
936
937 #[async_trait]
938 impl AsyncCommandHandler for AsyncHelloCommand {
939 async fn execute(
940 &self,
941 context: &mut dyn ExecutionContext,
942 args: &ParsedArgs,
943 ) -> Result<()> {
944 let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
945 ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
946 })?;
947 let name = args.get_scalar("name").unwrap_or("World");
948 ctx.state = format!("Hello, {}!", name);
949 Ok(())
950 }
951 }
952
953 /// Async command with custom validation, mirroring `ValidatedCommand`.
954 struct AsyncValidatedCommand;
955
956 #[async_trait]
957 impl AsyncCommandHandler for AsyncValidatedCommand {
958 async fn execute(
959 &self,
960 _context: &mut dyn ExecutionContext,
961 _args: &ParsedArgs,
962 ) -> Result<()> {
963 Ok(())
964 }
965
966 async fn validate(&self, args: &ParsedArgs) -> Result<()> {
967 if args.get_scalar("count").is_none() {
968 return Err(
969 ExecutionError::CommandFailed(anyhow::anyhow!("count is required")).into(),
970 );
971 }
972 Ok(())
973 }
974 }
975
976 /// Async command that fails during execution, mirroring `FailingCommand`.
977 struct AsyncFailingCommand;
978
979 #[async_trait]
980 impl AsyncCommandHandler for AsyncFailingCommand {
981 async fn execute(
982 &self,
983 _context: &mut dyn ExecutionContext,
984 _args: &ParsedArgs,
985 ) -> Result<()> {
986 Err(ExecutionError::CommandFailed(anyhow::anyhow!("Simulated async failure")).into())
987 }
988 }
989
990 #[test]
991 fn test_async_basic_execution() {
992 let handler = AsyncHelloCommand;
993 let mut context = TestContext::default();
994 let args = scalar_args([("name", "Rust")]);
995
996 let result = futures::executor::block_on(handler.execute(&mut context, &args));
997
998 assert!(result.is_ok());
999 assert_eq!(context.state, "Hello, Rust!");
1000 }
1001
1002 #[test]
1003 fn test_async_default_validation_accepts_all() {
1004 let handler = AsyncHelloCommand;
1005 let args = scalar_args([("random", "value")]);
1006
1007 let result = futures::executor::block_on(handler.validate(&args));
1008
1009 assert!(result.is_ok());
1010 }
1011
1012 #[test]
1013 fn test_async_custom_validation_missing_arg() {
1014 let handler = AsyncValidatedCommand;
1015 let args = ParsedArgs::from_scalars(HashMap::new());
1016
1017 let result = futures::executor::block_on(handler.validate(&args));
1018
1019 assert!(result.is_err());
1020 let err_msg = format!("{}", result.unwrap_err());
1021 assert!(err_msg.contains("required"));
1022 }
1023
1024 #[test]
1025 fn test_async_custom_validation_success() {
1026 let handler = AsyncValidatedCommand;
1027 let args = scalar_args([("count", "5")]);
1028
1029 let result = futures::executor::block_on(handler.validate(&args));
1030
1031 assert!(result.is_ok());
1032 }
1033
1034 #[test]
1035 fn test_async_execution_failure() {
1036 let handler = AsyncFailingCommand;
1037 let mut context = TestContext::default();
1038 let args = ParsedArgs::from_scalars(HashMap::new());
1039
1040 let result = futures::executor::block_on(handler.execute(&mut context, &args));
1041
1042 assert!(result.is_err());
1043 let err_msg = format!("{}", result.unwrap_err());
1044 assert!(err_msg.contains("Simulated async failure"));
1045 }
1046
1047 #[test]
1048 fn test_async_trait_object_usage() {
1049 // Verify that AsyncCommandHandler can be used as a trait object —
1050 // the core object-safety guarantee DD-022 depends on.
1051 let handler: Box<dyn AsyncCommandHandler> = Box::new(AsyncHelloCommand);
1052 let mut context = TestContext::default();
1053 let args = scalar_args([("name", "TraitObject")]);
1054
1055 let result = futures::executor::block_on(handler.execute(&mut context, &args));
1056
1057 assert!(result.is_ok());
1058 assert_eq!(context.state, "Hello, TraitObject!");
1059 }
1060
1061 #[test]
1062 fn test_async_send_sync_requirement() {
1063 // Verifies AsyncCommandHandler is Send + Sync by sharing it across
1064 // threads via Arc — same pattern as test_send_sync_requirement above.
1065 let handler: Arc<dyn AsyncCommandHandler> = Arc::new(AsyncHelloCommand);
1066 let handler_clone = handler.clone();
1067
1068 let _ = std::thread::spawn(move || {
1069 let _h = handler_clone;
1070 });
1071 }
1072
1073 #[test]
1074 fn test_async_object_safety_compile_time() {
1075 // If this compiles, AsyncCommandHandler is dyn-compatible.
1076 fn _accepts_trait_object(_: &dyn AsyncCommandHandler) {}
1077 _accepts_trait_object(&AsyncHelloCommand);
1078 }
1079
1080 // ============================================================================
1081 // expected_fault_tolerance() TESTS — async mirror (DD-028)
1082 // ============================================================================
1083
1084 /// Async handler whose author declares its failures must never be
1085 /// ignored — the async mirror of `FaultIntolerantCommand`.
1086 struct AsyncFaultIntolerantCommand;
1087
1088 #[async_trait]
1089 impl AsyncCommandHandler for AsyncFaultIntolerantCommand {
1090 async fn execute(
1091 &self,
1092 _context: &mut dyn ExecutionContext,
1093 _args: &ParsedArgs,
1094 ) -> Result<()> {
1095 Ok(())
1096 }
1097
1098 fn expected_fault_tolerance(&self) -> Option<bool> {
1099 Some(false)
1100 }
1101 }
1102
1103 #[test]
1104 fn test_async_expected_fault_tolerance_defaults_to_none() {
1105 // AsyncHelloCommand, defined before DD-028 existed, keeps compiling
1106 // and behaving exactly as before — no supertrait, nothing to add.
1107 assert_eq!(AsyncHelloCommand.expected_fault_tolerance(), None);
1108 }
1109
1110 #[test]
1111 fn test_async_expected_fault_tolerance_override() {
1112 assert_eq!(
1113 AsyncFaultIntolerantCommand.expected_fault_tolerance(),
1114 Some(false)
1115 );
1116 }
1117
1118 #[test]
1119 fn test_async_expected_fault_tolerance_via_trait_object() {
1120 // Reachable through `Box<dyn AsyncCommandHandler>` — what
1121 // `CommandRegistry::register_async` actually calls it through.
1122 let handler: Box<dyn AsyncCommandHandler> = Box::new(AsyncFaultIntolerantCommand);
1123 assert_eq!(handler.expected_fault_tolerance(), Some(false));
1124 }
1125}