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 (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}