Skip to main content

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
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///    — before any of the steps below, and before this handler ever sees
85///    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`, if the handler's author has one.
275    ///
276    /// Purely informational at the trait level: `CommandRegistry::register_sync`
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    /// — 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` — 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.
343///
344/// `dynamic-cli` does not depend on `tokio` or any other async runtime.
345/// The CLI and REPL dispatch loops are strictly sequential (each command
346/// runs to completion before the next input is read), so the dispatch
347/// site drives the returned future with `futures::executor::block_on`.
348/// The future borrows the execution context, so it could not be spawned
349/// onto a runtime anyway. A handler that needs a runtime, e.g. for
350/// network I/O, uses its own inside `execute`.
351///
352/// # Example
353///
354/// ```
355/// use async_trait::async_trait;
356/// use dynamic_cli::executor::{AsyncCommandHandler, ParsedArgs};
357/// use dynamic_cli::context::ExecutionContext;
358/// use dynamic_cli::error::ExecutionError;
359/// use dynamic_cli::Result;
360///
361/// struct FetchCommand;
362///
363/// #[async_trait]
364/// impl AsyncCommandHandler for FetchCommand {
365///     async fn execute(
366///         &self,
367///         _context: &mut dyn ExecutionContext,
368///         args: &ParsedArgs,
369///     ) -> Result<()> {
370///         let url = args.get_scalar("url").ok_or_else(|| {
371///             ExecutionError::CommandFailed(anyhow::anyhow!("Missing 'url' argument"))
372///         })?;
373///         // Real implementations would `.await` an async HTTP call here.
374///         println!("Fetching {url}...");
375///         Ok(())
376///     }
377///
378///     async fn validate(&self, args: &ParsedArgs) -> Result<()> {
379///         if args.get_scalar("url").is_none() {
380///             return Err(ExecutionError::CommandFailed(anyhow::anyhow!("url is required")).into());
381///         }
382///         Ok(())
383///     }
384/// }
385/// ```
386#[async_trait]
387pub trait AsyncCommandHandler: Send + Sync {
388    /// Async equivalent of [`CommandHandler::execute`]. Same contract:
389    /// receives the mutable execution context and the parsed arguments,
390    /// returns `Ok(())` on success or a `DynamicCliError` on failure.
391    async fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()>;
392
393    /// Async equivalent of [`CommandHandler::validate`]. Same contract and
394    /// same default (accepts all arguments) — override only for custom
395    /// validation logic.
396    async fn validate(&self, _args: &ParsedArgs) -> Result<()> {
397        Ok(())
398    }
399
400    /// Mirrors [`CommandHandler::expected_fault_tolerance`] exactly — same
401    /// contract, same default (`None`), checked by
402    /// `CommandRegistry::register_async` the same way `register_sync` checks
403    /// it for [`CommandHandler`].
404    ///
405    /// Deliberately **not** an `async fn`, unlike `execute`/`validate`
406    /// above: this method does no work of its own and is evaluated at
407    /// registration time, before any dispatch — sync or async — so making
408    /// it async would only force `register_async` (itself a synchronous
409    /// function) to `block_on` it for no benefit.
410    fn expected_fault_tolerance(&self) -> Option<bool> {
411        None
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use crate::error::ExecutionError;
419    use std::any::Any;
420    use std::collections::HashMap;
421    use std::sync::Arc;
422
423    /// Test helper: build a scalar-only `ParsedArgs` from `[(key, value), ...]`.
424    fn scalar_args<const N: usize>(pairs: [(&str, &str); N]) -> ParsedArgs {
425        let map: HashMap<String, String> = pairs
426            .into_iter()
427            .map(|(k, v)| (k.to_string(), v.to_string()))
428            .collect();
429        ParsedArgs::from_scalars(map)
430    }
431
432    // ============================================================================
433    // TEST FIXTURES
434    // ============================================================================
435
436    /// Simple test context for unit tests
437    #[derive(Default)]
438    struct TestContext {
439        state: String,
440    }
441
442    impl ExecutionContext for TestContext {
443        fn as_any(&self) -> &dyn Any {
444            self
445        }
446
447        fn as_any_mut(&mut self) -> &mut dyn Any {
448            self
449        }
450    }
451
452    /// Simple command that prints to context
453    struct HelloCommand;
454
455    impl CommandHandler for HelloCommand {
456        fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()> {
457            let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
458                ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
459            })?;
460
461            let name = args.get_scalar("name").unwrap_or("World");
462            ctx.state = format!("Hello, {}!", name);
463            Ok(())
464        }
465    }
466
467    /// Command with custom validation
468    struct ValidatedCommand;
469
470    impl CommandHandler for ValidatedCommand {
471        fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
472            Ok(())
473        }
474
475        fn validate(&self, args: &ParsedArgs) -> Result<()> {
476            // Require "count" argument to be present and > 0
477            if let Some(count) = args.get_scalar("count") {
478                let count_val: i32 = count.parse().map_err(|_| {
479                    ExecutionError::CommandFailed(anyhow::anyhow!("count must be an integer"))
480                })?;
481
482                if count_val <= 0 {
483                    return Err(ExecutionError::CommandFailed(anyhow::anyhow!(
484                        "count must be positive"
485                    ))
486                    .into());
487                }
488            } else {
489                return Err(
490                    ExecutionError::CommandFailed(anyhow::anyhow!("count is required")).into(),
491                );
492            }
493            Ok(())
494        }
495    }
496
497    /// Command that fails during execution
498    struct FailingCommand;
499
500    impl CommandHandler for FailingCommand {
501        fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
502            Err(ExecutionError::CommandFailed(anyhow::anyhow!("Simulated failure")).into())
503        }
504    }
505
506    /// Command that modifies context
507    struct StatefulCommand;
508
509    impl CommandHandler for StatefulCommand {
510        fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()> {
511            let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
512                ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
513            })?;
514
515            let value = args.get_scalar("value").unwrap_or("default");
516            ctx.state.push_str(value);
517            Ok(())
518        }
519    }
520
521    // ============================================================================
522    // BASIC FUNCTIONALITY TESTS
523    // ============================================================================
524
525    #[test]
526    fn test_basic_execution() {
527        let handler = HelloCommand;
528        let mut context = TestContext::default();
529        let args = scalar_args([("name", "Rust")]);
530
531        let result = handler.execute(&mut context, &args);
532
533        assert!(result.is_ok());
534        assert_eq!(context.state, "Hello, Rust!");
535    }
536
537    #[test]
538    fn test_execution_without_args() {
539        let handler = HelloCommand;
540        let mut context = TestContext::default();
541        let args = ParsedArgs::from_scalars(HashMap::new());
542
543        let result = handler.execute(&mut context, &args);
544
545        assert!(result.is_ok());
546        assert_eq!(context.state, "Hello, World!");
547    }
548
549    #[test]
550    fn test_execution_with_empty_name() {
551        let handler = HelloCommand;
552        let mut context = TestContext::default();
553        let args = scalar_args([("name", "")]);
554
555        let result = handler.execute(&mut context, &args);
556
557        assert!(result.is_ok());
558        assert_eq!(context.state, "Hello, !");
559    }
560
561    // ============================================================================
562    // VALIDATION TESTS
563    // ============================================================================
564
565    #[test]
566    fn test_default_validation_accepts_all() {
567        let handler = HelloCommand;
568        let args = scalar_args([("random", "value")]);
569
570        let result = handler.validate(&args);
571
572        assert!(result.is_ok());
573    }
574
575    #[test]
576    fn test_custom_validation_success() {
577        let handler = ValidatedCommand;
578        let args = scalar_args([("count", "5")]);
579
580        let result = handler.validate(&args);
581
582        assert!(result.is_ok());
583    }
584
585    #[test]
586    fn test_custom_validation_missing_arg() {
587        let handler = ValidatedCommand;
588        let args = ParsedArgs::from_scalars(HashMap::new());
589
590        let result = handler.validate(&args);
591
592        assert!(result.is_err());
593        let err_msg = format!("{}", result.unwrap_err());
594        assert!(err_msg.contains("required"));
595    }
596
597    #[test]
598    fn test_custom_validation_invalid_value() {
599        let handler = ValidatedCommand;
600        let args = scalar_args([("count", "0")]);
601
602        let result = handler.validate(&args);
603
604        assert!(result.is_err());
605        let err_msg = format!("{}", result.unwrap_err());
606        assert!(err_msg.contains("positive"));
607    }
608
609    #[test]
610    fn test_custom_validation_non_integer() {
611        let handler = ValidatedCommand;
612        let args = scalar_args([("count", "abc")]);
613
614        let result = handler.validate(&args);
615
616        assert!(result.is_err());
617        let err_msg = format!("{}", result.unwrap_err());
618        assert!(err_msg.contains("integer"));
619    }
620
621    // ============================================================================
622    // ERROR HANDLING TESTS
623    // ============================================================================
624
625    #[test]
626    fn test_execution_failure() {
627        let handler = FailingCommand;
628        let mut context = TestContext::default();
629        let args = ParsedArgs::from_scalars(HashMap::new());
630
631        let result = handler.execute(&mut context, &args);
632
633        assert!(result.is_err());
634        let err_msg = format!("{}", result.unwrap_err());
635        assert!(err_msg.contains("Simulated failure"));
636    }
637
638    #[test]
639    fn test_context_downcast_failure() {
640        // Use a different context type to trigger downcast failure
641        struct WrongContext;
642
643        impl ExecutionContext for WrongContext {
644            fn as_any(&self) -> &dyn Any {
645                self
646            }
647
648            fn as_any_mut(&mut self) -> &mut dyn Any {
649                self
650            }
651        }
652
653        let handler = HelloCommand;
654        let mut wrong_context = WrongContext;
655        let args = ParsedArgs::from_scalars(HashMap::new());
656
657        let result = handler.execute(&mut wrong_context, &args);
658
659        assert!(result.is_err());
660        let err_msg = format!("{}", result.unwrap_err());
661        assert!(err_msg.contains("Wrong context type"));
662    }
663
664    // ============================================================================
665    // STATE MODIFICATION TESTS
666    // ============================================================================
667
668    #[test]
669    fn test_context_state_modification() {
670        let handler = StatefulCommand;
671        let mut context = TestContext {
672            state: "initial".to_string(),
673        };
674        let args = scalar_args([("value", "_modified")]);
675
676        let result = handler.execute(&mut context, &args);
677
678        assert!(result.is_ok());
679        assert_eq!(context.state, "initial_modified");
680    }
681
682    #[test]
683    fn test_multiple_executions_preserve_state() {
684        let handler = StatefulCommand;
685        let mut context = TestContext::default();
686
687        // First execution
688        let args1 = scalar_args([("value", "first")]);
689        handler.execute(&mut context, &args1).unwrap();
690        assert_eq!(context.state, "first");
691
692        // Second execution
693        let args2 = scalar_args([("value", "_second")]);
694        handler.execute(&mut context, &args2).unwrap();
695        assert_eq!(context.state, "first_second");
696    }
697
698    // ============================================================================
699    // TRAIT OBJECT TESTS
700    // ============================================================================
701
702    #[test]
703    fn test_trait_object_usage() {
704        // Verify that CommandHandler can be used as a trait object
705        let handler: Box<dyn CommandHandler> = Box::new(HelloCommand);
706        let mut context = TestContext::default();
707        let args = scalar_args([("name", "TraitObject")]);
708
709        let result = handler.execute(&mut context, &args);
710
711        assert!(result.is_ok());
712        assert_eq!(context.state, "Hello, TraitObject!");
713    }
714
715    #[test]
716    fn test_multiple_trait_objects() {
717        // Store multiple handlers as trait objects
718        let handlers: Vec<Box<dyn CommandHandler>> =
719            vec![Box::new(HelloCommand), Box::new(StatefulCommand)];
720
721        let mut context = TestContext::default();
722
723        // Execute first handler
724        let args1 = scalar_args([("name", "First")]);
725        handlers[0].execute(&mut context, &args1).unwrap();
726        assert_eq!(context.state, "Hello, First!");
727
728        // Execute second handler
729        context.state.clear();
730        let args2 = scalar_args([("value", "Second")]);
731        handlers[1].execute(&mut context, &args2).unwrap();
732        assert_eq!(context.state, "Second");
733    }
734
735    // ============================================================================
736    // THREAD SAFETY TESTS
737    // ============================================================================
738
739    #[test]
740    fn test_send_sync_requirement() {
741        // This test verifies that CommandHandler is Send + Sync
742        // by using it in a multi-threaded context
743        let handler: Arc<dyn CommandHandler> = Arc::new(HelloCommand);
744
745        // Clone the Arc to simulate sharing across threads
746        let handler_clone = handler.clone();
747
748        // This compilation test ensures Send + Sync are satisfied
749        let _ = std::thread::spawn(move || {
750            let _h = handler_clone;
751        });
752    }
753
754    #[test]
755    fn test_concurrent_validation() {
756        // Test that validation can be called from multiple threads
757        let handler = Arc::new(ValidatedCommand);
758        let handler_clone = handler.clone();
759
760        let handle = std::thread::spawn(move || {
761            let args = scalar_args([("count", "10")]);
762            handler_clone.validate(&args)
763        });
764
765        let args = scalar_args([("count", "5")]);
766        let result1 = handler.validate(&args);
767
768        let result2 = handle.join().unwrap();
769
770        assert!(result1.is_ok());
771        assert!(result2.is_ok());
772    }
773
774    // ============================================================================
775    // EDGE CASES
776    // ============================================================================
777
778    #[test]
779    fn test_empty_args() {
780        let handler = StatefulCommand;
781        let mut context = TestContext::default();
782        let args = ParsedArgs::from_scalars(HashMap::new());
783
784        // Should use default value
785        let result = handler.execute(&mut context, &args);
786
787        assert!(result.is_ok());
788        assert_eq!(context.state, "default");
789    }
790
791    #[test]
792    fn test_args_with_special_characters() {
793        let handler = HelloCommand;
794        let mut context = TestContext::default();
795        let args = scalar_args([("name", "Hello, 世界! 🌍")]);
796
797        let result = handler.execute(&mut context, &args);
798
799        assert!(result.is_ok());
800        assert_eq!(context.state, "Hello, Hello, 世界! 🌍!");
801    }
802
803    #[test]
804    fn test_very_long_argument() {
805        let handler = HelloCommand;
806        let mut context = TestContext::default();
807        let long_name = "x".repeat(10000);
808        let args = scalar_args([("name", long_name.as_str())]);
809
810        let result = handler.execute(&mut context, &args);
811
812        assert!(result.is_ok());
813        assert!(context.state.contains(&long_name));
814    }
815
816    // ============================================================================
817    // SHARED STATE TESTS
818    // ============================================================================
819
820    #[test]
821    fn test_shared_mutable_context() {
822        // Test that context can be safely modified by multiple commands
823        let handler1 = StatefulCommand;
824        let handler2 = StatefulCommand;
825        let mut context = TestContext::default();
826
827        let args1 = scalar_args([("value", "A")]);
828        handler1.execute(&mut context, &args1).unwrap();
829
830        let args2 = scalar_args([("value", "B")]);
831        handler2.execute(&mut context, &args2).unwrap();
832
833        assert_eq!(context.state, "AB");
834    }
835
836    // Test to ensure the trait is indeed object-safe at compile time
837    #[test]
838    fn test_object_safety_compile_time() {
839        // This function signature requires CommandHandler to be object-safe
840        fn _accepts_trait_object(_: &dyn CommandHandler) {}
841
842        // If this compiles, the trait is object-safe
843        let handler = HelloCommand;
844        _accepts_trait_object(&handler);
845    }
846
847    // Test that demonstrates why we can't have generic methods
848    // (This is a documentation test, not an actual test that runs)
849    /// ```compile_fail
850    /// use dynamic_cli::executor::CommandHandler;
851    ///
852    /// trait BrokenHandler: CommandHandler {
853    ///     fn generic_method<T>(&self, value: T);
854    /// }
855    ///
856    /// // This would fail because trait objects can't have generic methods
857    /// fn use_as_trait_object(handler: &dyn BrokenHandler) {
858    ///     // Cannot call generic_method on trait object
859    /// }
860    /// ```
861    #[allow(dead_code)]
862    fn test_no_generic_methods_documentation() {}
863
864    // ============================================================================
865    // expected_fault_tolerance() TESTS — DD-028 (#64)
866    // ============================================================================
867
868    /// A handler whose author never expressed an opinion — the common case.
869    struct FaultAgnosticCommand;
870
871    impl CommandHandler for FaultAgnosticCommand {
872        fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
873            Ok(())
874        }
875    }
876
877    /// A handler whose author declares its failures must never be ignored.
878    struct FaultIntolerantCommand;
879
880    impl CommandHandler for FaultIntolerantCommand {
881        fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
882            Ok(())
883        }
884
885        fn expected_fault_tolerance(&self) -> Option<bool> {
886            Some(false)
887        }
888    }
889
890    /// A handler whose author declares its failures are safe to ignore.
891    struct FaultTolerantCommand;
892
893    impl CommandHandler for FaultTolerantCommand {
894        fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
895            Ok(())
896        }
897
898        fn expected_fault_tolerance(&self) -> Option<bool> {
899            Some(true)
900        }
901    }
902
903    #[test]
904    fn test_expected_fault_tolerance_defaults_to_none() {
905        // Every pre-existing handler written before DD-028 (#64) keeps
906        // compiling and behaving exactly as before — no supertrait, no
907        // required impl, nothing to add.
908        let handler = FaultAgnosticCommand;
909        assert_eq!(handler.expected_fault_tolerance(), None);
910
911        // HelloCommand and ValidatedCommand, defined before DD-028 (#64),
912        // are just as unaffected.
913        assert_eq!(HelloCommand.expected_fault_tolerance(), None);
914        assert_eq!(ValidatedCommand.expected_fault_tolerance(), None);
915    }
916
917    #[test]
918    fn test_expected_fault_tolerance_override() {
919        assert_eq!(
920            FaultIntolerantCommand.expected_fault_tolerance(),
921            Some(false)
922        );
923        assert_eq!(FaultTolerantCommand.expected_fault_tolerance(), Some(true));
924    }
925
926    #[test]
927    fn test_expected_fault_tolerance_via_trait_object() {
928        // The method must be reachable through `Box<dyn CommandHandler>`,
929        // exactly like `execute`/`validate` — this is what
930        // `CommandRegistry::register_sync` actually calls it through.
931        let handler: Box<dyn CommandHandler> = Box::new(FaultIntolerantCommand);
932        assert_eq!(handler.expected_fault_tolerance(), Some(false));
933    }
934
935    // ============================================================================
936    // AsyncCommandHandler TESTS — DA-022 (#8)
937    // ============================================================================
938
939    /// Async command that writes to the test context, mirroring `HelloCommand`.
940    struct AsyncHelloCommand;
941
942    #[async_trait]
943    impl AsyncCommandHandler for AsyncHelloCommand {
944        async fn execute(
945            &self,
946            context: &mut dyn ExecutionContext,
947            args: &ParsedArgs,
948        ) -> Result<()> {
949            let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
950                ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
951            })?;
952            let name = args.get_scalar("name").unwrap_or("World");
953            ctx.state = format!("Hello, {}!", name);
954            Ok(())
955        }
956    }
957
958    /// Async command with custom validation, mirroring `ValidatedCommand`.
959    struct AsyncValidatedCommand;
960
961    #[async_trait]
962    impl AsyncCommandHandler for AsyncValidatedCommand {
963        async fn execute(
964            &self,
965            _context: &mut dyn ExecutionContext,
966            _args: &ParsedArgs,
967        ) -> Result<()> {
968            Ok(())
969        }
970
971        async fn validate(&self, args: &ParsedArgs) -> Result<()> {
972            if args.get_scalar("count").is_none() {
973                return Err(
974                    ExecutionError::CommandFailed(anyhow::anyhow!("count is required")).into(),
975                );
976            }
977            Ok(())
978        }
979    }
980
981    /// Async command that fails during execution, mirroring `FailingCommand`.
982    struct AsyncFailingCommand;
983
984    #[async_trait]
985    impl AsyncCommandHandler for AsyncFailingCommand {
986        async fn execute(
987            &self,
988            _context: &mut dyn ExecutionContext,
989            _args: &ParsedArgs,
990        ) -> Result<()> {
991            Err(ExecutionError::CommandFailed(anyhow::anyhow!("Simulated async failure")).into())
992        }
993    }
994
995    #[test]
996    fn test_async_basic_execution() {
997        let handler = AsyncHelloCommand;
998        let mut context = TestContext::default();
999        let args = scalar_args([("name", "Rust")]);
1000
1001        let result = futures::executor::block_on(handler.execute(&mut context, &args));
1002
1003        assert!(result.is_ok());
1004        assert_eq!(context.state, "Hello, Rust!");
1005    }
1006
1007    #[test]
1008    fn test_async_default_validation_accepts_all() {
1009        let handler = AsyncHelloCommand;
1010        let args = scalar_args([("random", "value")]);
1011
1012        let result = futures::executor::block_on(handler.validate(&args));
1013
1014        assert!(result.is_ok());
1015    }
1016
1017    #[test]
1018    fn test_async_custom_validation_missing_arg() {
1019        let handler = AsyncValidatedCommand;
1020        let args = ParsedArgs::from_scalars(HashMap::new());
1021
1022        let result = futures::executor::block_on(handler.validate(&args));
1023
1024        assert!(result.is_err());
1025        let err_msg = format!("{}", result.unwrap_err());
1026        assert!(err_msg.contains("required"));
1027    }
1028
1029    #[test]
1030    fn test_async_custom_validation_success() {
1031        let handler = AsyncValidatedCommand;
1032        let args = scalar_args([("count", "5")]);
1033
1034        let result = futures::executor::block_on(handler.validate(&args));
1035
1036        assert!(result.is_ok());
1037    }
1038
1039    #[test]
1040    fn test_async_execution_failure() {
1041        let handler = AsyncFailingCommand;
1042        let mut context = TestContext::default();
1043        let args = ParsedArgs::from_scalars(HashMap::new());
1044
1045        let result = futures::executor::block_on(handler.execute(&mut context, &args));
1046
1047        assert!(result.is_err());
1048        let err_msg = format!("{}", result.unwrap_err());
1049        assert!(err_msg.contains("Simulated async failure"));
1050    }
1051
1052    #[test]
1053    fn test_async_trait_object_usage() {
1054        // Verify that AsyncCommandHandler can be used as a trait object —
1055        // the core object-safety guarantee DA-022 (#8) depends on.
1056        let handler: Box<dyn AsyncCommandHandler> = Box::new(AsyncHelloCommand);
1057        let mut context = TestContext::default();
1058        let args = scalar_args([("name", "TraitObject")]);
1059
1060        let result = futures::executor::block_on(handler.execute(&mut context, &args));
1061
1062        assert!(result.is_ok());
1063        assert_eq!(context.state, "Hello, TraitObject!");
1064    }
1065
1066    #[test]
1067    fn test_async_send_sync_requirement() {
1068        // Verifies AsyncCommandHandler is Send + Sync by sharing it across
1069        // threads via Arc — same pattern as test_send_sync_requirement above.
1070        let handler: Arc<dyn AsyncCommandHandler> = Arc::new(AsyncHelloCommand);
1071        let handler_clone = handler.clone();
1072
1073        let _ = std::thread::spawn(move || {
1074            let _h = handler_clone;
1075        });
1076    }
1077
1078    #[test]
1079    fn test_async_object_safety_compile_time() {
1080        // If this compiles, AsyncCommandHandler is dyn-compatible.
1081        fn _accepts_trait_object(_: &dyn AsyncCommandHandler) {}
1082        _accepts_trait_object(&AsyncHelloCommand);
1083    }
1084
1085    // ============================================================================
1086    // expected_fault_tolerance() TESTS — async mirror, DD-028 (#64)
1087    // ============================================================================
1088
1089    /// Async handler whose author declares its failures must never be
1090    /// ignored — the async mirror of `FaultIntolerantCommand`.
1091    struct AsyncFaultIntolerantCommand;
1092
1093    #[async_trait]
1094    impl AsyncCommandHandler for AsyncFaultIntolerantCommand {
1095        async fn execute(
1096            &self,
1097            _context: &mut dyn ExecutionContext,
1098            _args: &ParsedArgs,
1099        ) -> Result<()> {
1100            Ok(())
1101        }
1102
1103        fn expected_fault_tolerance(&self) -> Option<bool> {
1104            Some(false)
1105        }
1106    }
1107
1108    #[test]
1109    fn test_async_expected_fault_tolerance_defaults_to_none() {
1110        // AsyncHelloCommand, defined before DD-028 (#64), keeps compiling
1111        // and behaving exactly as before — no supertrait, nothing to add.
1112        assert_eq!(AsyncHelloCommand.expected_fault_tolerance(), None);
1113    }
1114
1115    #[test]
1116    fn test_async_expected_fault_tolerance_override() {
1117        assert_eq!(
1118            AsyncFaultIntolerantCommand.expected_fault_tolerance(),
1119            Some(false)
1120        );
1121    }
1122
1123    #[test]
1124    fn test_async_expected_fault_tolerance_via_trait_object() {
1125        // Reachable through `Box<dyn AsyncCommandHandler>` — what
1126        // `CommandRegistry::register_async` actually calls it through.
1127        let handler: Box<dyn AsyncCommandHandler> = Box::new(AsyncFaultIntolerantCommand);
1128        assert_eq!(handler.expected_fault_tolerance(), Some(false));
1129    }
1130}