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/// 1. Parser converts user input to [`crate::parser::ParsedArgs`]
84/// 2. Validator checks argument constraints
85/// 3. `validate()` is called for custom validation (optional)
86/// 4. `execute()` is called with validated arguments
87///
88/// # Example
89///
90/// ```
91/// use dynamic_cli::error::ExecutionError;
92/// use dynamic_cli::executor::{CommandHandler, ParsedArgs};
93/// use dynamic_cli::context::ExecutionContext;
94/// use dynamic_cli::Result;
95///
96/// struct GreetCommand;
97///
98/// impl CommandHandler for GreetCommand {
99///     fn execute(
100///         &self,
101///         _context: &mut dyn ExecutionContext,
102///         args: &ParsedArgs,
103///     ) -> Result<()> {
104///         let name = args.get_scalar("name")
105///             .ok_or_else(|| {
106///                 ExecutionError::CommandFailed(
107///                     anyhow::anyhow!("Missing 'name' argument")
108///              )
109///          })?;
110///         
111///         let greeting = if let Some(formal) = args.get_scalar("formal") {
112///             if formal == "true" {
113///                 format!("Good day, {}.", name)
114///             } else {
115///                 format!("Hi, {}!", name)
116///             }
117///         } else {
118///             format!("Hello, {}!", name)
119///         };
120///         
121///         println!("{}", greeting);
122///         Ok(())
123///     }
124///     
125///     fn validate(&self, args: &ParsedArgs) -> Result<()> {
126///         // Custom validation: name must not be empty
127///         if let Some(name) = args.get_scalar("name") {
128///             if name.trim().is_empty() {
129///                 return Err(ExecutionError::CommandFailed(
130///                         anyhow::anyhow!("Name cannot be empty")
131///                 ).into());
132///             }
133///         }
134///         Ok(())
135///     }
136/// }
137/// ```
138pub trait CommandHandler: Send + Sync {
139    /// Execute the command with the given context and arguments
140    ///
141    /// This is the main entry point for command execution. It receives:
142    /// - A mutable reference to the execution context (for shared state)
143    /// - A map of argument names to their string values
144    ///
145    /// # Arguments
146    ///
147    /// * `context` - Mutable execution context for sharing state between commands.
148    ///   Use `downcast_ref` or `downcast_mut` from the `context` module
149    ///   to access your specific context type.
150    ///
151    /// * `args` - Parsed and validated arguments as name-value pairs.
152    ///   All values are strings; type conversion should be done
153    ///   within the handler if needed.
154    ///
155    /// # Returns
156    ///
157    /// - `Ok(())` if execution succeeds
158    /// - `Err(DynamicCliError)` if execution fails
159    ///
160    /// # Errors
161    ///
162    /// Implementations should return errors for:
163    /// - Invalid argument values (caught by validate, but can be rechecked)
164    /// - Execution failures (I/O errors, computation errors, etc.)
165    /// - Invalid context state
166    ///
167    /// Use `ExecutionError::CommandFailed` to wrap application-specific errors:
168    /// ```ignore
169    /// Err(ExecutionError::CommandFailed(anyhow::anyhow!("Details")).into())
170    /// ```
171    ///
172    /// # Example
173    ///
174    /// ```
175    /// # use dynamic_cli::error::ExecutionError;
176    /// # use dynamic_cli::executor::{CommandHandler, ParsedArgs};
177    /// # use dynamic_cli::context::ExecutionContext;
178    /// # use dynamic_cli::Result;
179    /// #
180    /// struct FileCommand;
181    ///
182    /// impl CommandHandler for FileCommand {
183    ///     fn execute(
184    ///         &self,
185    ///         _context: &mut dyn ExecutionContext,
186    ///         args: &ParsedArgs,
187    ///     ) -> Result<()> {
188    ///         let path = args.get_scalar("path")
189    ///             .ok_or_else(|| {
190    ///                ExecutionError::CommandFailed(
191    ///                       anyhow::anyhow!("Missing path argument")
192    ///             )
193    ///          })?;
194    ///         
195    ///         // Perform the actual work
196    ///         let content = std::fs::read_to_string(path)
197    ///             .map_err(|e| {
198    ///                ExecutionError::CommandFailed(anyhow::anyhow!("Failed to read file: {}", e))
199    ///         })?;
200    ///         
201    ///         println!("File contains {} bytes", content.len());
202    ///         Ok(())
203    ///     }
204    /// }
205    /// ```
206    fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()>;
207
208    /// Optional custom validation for arguments
209    ///
210    /// This method is called after the standard validation (type checking,
211    /// required arguments, etc.) but before execution. It allows commands
212    /// to implement custom validation logic.
213    ///
214    /// # Default Implementation
215    ///
216    /// The default implementation accepts all arguments (returns `Ok(())`).
217    /// Override this method only if you need custom validation.
218    ///
219    /// # Arguments
220    ///
221    /// * `args` - The arguments to validate
222    ///
223    /// # Returns
224    ///
225    /// - `Ok(())` if validation succeeds
226    /// - `Err(DynamicCliError)` if validation fails
227    ///
228    /// # Example
229    ///
230    /// ```
231    /// # use dynamic_cli::executor::{CommandHandler, ParsedArgs};
232    /// # use dynamic_cli::context::ExecutionContext;
233    /// # use dynamic_cli::error::ExecutionError;
234    /// # use dynamic_cli::Result;
235    /// #
236    /// struct RangeCommand;
237    ///
238    /// impl CommandHandler for RangeCommand {
239    ///     fn execute(
240    ///         &self,
241    ///         _context: &mut dyn ExecutionContext,
242    ///         args: &ParsedArgs,
243    ///     ) -> Result<()> {
244    ///         // Execution logic here
245    ///         Ok(())
246    ///     }
247    ///     
248    ///     fn validate(&self, args: &ParsedArgs) -> Result<()> {
249    ///         // Custom validation: ensure min < max
250    ///         if let (Some(min), Some(max)) = (args.get_scalar("min"), args.get_scalar("max")) {
251    ///             let min_val: f64 = min.parse()
252    ///                 .map_err(|_| {
253    ///                     ExecutionError::CommandFailed(anyhow::anyhow!("Invalid min value"))
254    ///             })?;
255    ///             let max_val: f64 = max.parse()
256    ///                 .map_err(|_| {ExecutionError::CommandFailed(anyhow::anyhow!("Invalid max value"))})?;
257    ///             
258    ///             if min_val >= max_val {
259    ///                 return Err(ExecutionError::CommandFailed(anyhow::anyhow!("min must be less than max")).into());
260    ///             }
261    ///         }
262    ///         Ok(())
263    ///     }
264    /// }
265    /// ```
266    fn validate(&self, _args: &ParsedArgs) -> Result<()> {
267        Ok(())
268    }
269}
270
271/// Async counterpart of [`CommandHandler`].
272///
273/// Additive to `CommandHandler` (see DD-022) β€” it does not replace it.
274/// Implementations use this trait when their command body needs to perform
275/// async I/O (network calls, streaming, etc.). The signatures deliberately
276/// mirror `CommandHandler` exactly, `execute`/`validate` aside from the
277/// `async` keyword, so that migrating a handler from sync to async is a
278/// mechanical change.
279///
280/// # Object Safety
281///
282/// Made `dyn`-compatible via `#[async_trait]` (which desugars `async fn` to
283/// a boxed, pinned future under the hood). Stored as `Box<dyn
284/// AsyncCommandHandler>` in the registry, exactly like `CommandHandler` is
285/// stored as `Box<dyn CommandHandler>`.
286///
287/// # Thread Safety
288///
289/// Same constraint as `CommandHandler`: `Send + Sync` is required so the
290/// handler can be shared across the registry and, transitively, across
291/// threads if the application needs it.
292///
293/// # Why a separate trait instead of an async `CommandHandler`?
294///
295/// Existing sync `CommandHandler` implementations (including downstream
296/// consumers) must keep compiling unchanged. See DD-022 for the full
297/// rationale, including why `tokio` is not a dependency of `dynamic-cli`
298/// itself and why driving the returned future via
299/// `futures::executor::block_on` at the dispatch site is safe.
300///
301/// # Example
302///
303/// ```
304/// use async_trait::async_trait;
305/// use dynamic_cli::executor::{AsyncCommandHandler, ParsedArgs};
306/// use dynamic_cli::context::ExecutionContext;
307/// use dynamic_cli::error::ExecutionError;
308/// use dynamic_cli::Result;
309///
310/// struct FetchCommand;
311///
312/// #[async_trait]
313/// impl AsyncCommandHandler for FetchCommand {
314///     async fn execute(
315///         &self,
316///         _context: &mut dyn ExecutionContext,
317///         args: &ParsedArgs,
318///     ) -> Result<()> {
319///         let url = args.get_scalar("url").ok_or_else(|| {
320///             ExecutionError::CommandFailed(anyhow::anyhow!("Missing 'url' argument"))
321///         })?;
322///         // Real implementations would `.await` an async HTTP call here.
323///         println!("Fetching {url}...");
324///         Ok(())
325///     }
326///
327///     async fn validate(&self, args: &ParsedArgs) -> Result<()> {
328///         if args.get_scalar("url").is_none() {
329///             return Err(ExecutionError::CommandFailed(anyhow::anyhow!("url is required")).into());
330///         }
331///         Ok(())
332///     }
333/// }
334/// ```
335#[async_trait]
336pub trait AsyncCommandHandler: Send + Sync {
337    /// Async equivalent of [`CommandHandler::execute`]. Same contract:
338    /// receives the mutable execution context and the parsed arguments,
339    /// returns `Ok(())` on success or a `DynamicCliError` on failure.
340    async fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()>;
341
342    /// Async equivalent of [`CommandHandler::validate`]. Same contract and
343    /// same default (accepts all arguments) β€” override only for custom
344    /// validation logic.
345    async fn validate(&self, _args: &ParsedArgs) -> Result<()> {
346        Ok(())
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353    use crate::error::ExecutionError;
354    use std::any::Any;
355    use std::collections::HashMap;
356    use std::sync::Arc;
357
358    /// Test helper: build a scalar-only `ParsedArgs` from `[(key, value), ...]`.
359    fn scalar_args<const N: usize>(pairs: [(&str, &str); N]) -> ParsedArgs {
360        let map: HashMap<String, String> = pairs
361            .into_iter()
362            .map(|(k, v)| (k.to_string(), v.to_string()))
363            .collect();
364        ParsedArgs::from_scalars(map)
365    }
366
367    // ============================================================================
368    // TEST FIXTURES
369    // ============================================================================
370
371    /// Simple test context for unit tests
372    #[derive(Default)]
373    struct TestContext {
374        state: String,
375    }
376
377    impl ExecutionContext for TestContext {
378        fn as_any(&self) -> &dyn Any {
379            self
380        }
381
382        fn as_any_mut(&mut self) -> &mut dyn Any {
383            self
384        }
385    }
386
387    /// Simple command that prints to context
388    struct HelloCommand;
389
390    impl CommandHandler for HelloCommand {
391        fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()> {
392            let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
393                ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
394            })?;
395
396            let name = args.get_scalar("name").unwrap_or("World");
397            ctx.state = format!("Hello, {}!", name);
398            Ok(())
399        }
400    }
401
402    /// Command with custom validation
403    struct ValidatedCommand;
404
405    impl CommandHandler for ValidatedCommand {
406        fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
407            Ok(())
408        }
409
410        fn validate(&self, args: &ParsedArgs) -> Result<()> {
411            // Require "count" argument to be present and > 0
412            if let Some(count) = args.get_scalar("count") {
413                let count_val: i32 = count.parse().map_err(|_| {
414                    ExecutionError::CommandFailed(anyhow::anyhow!("count must be an integer"))
415                })?;
416
417                if count_val <= 0 {
418                    return Err(ExecutionError::CommandFailed(anyhow::anyhow!(
419                        "count must be positive"
420                    ))
421                    .into());
422                }
423            } else {
424                return Err(
425                    ExecutionError::CommandFailed(anyhow::anyhow!("count is required")).into(),
426                );
427            }
428            Ok(())
429        }
430    }
431
432    /// Command that fails during execution
433    struct FailingCommand;
434
435    impl CommandHandler for FailingCommand {
436        fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
437            Err(ExecutionError::CommandFailed(anyhow::anyhow!("Simulated failure")).into())
438        }
439    }
440
441    /// Command that modifies context
442    struct StatefulCommand;
443
444    impl CommandHandler for StatefulCommand {
445        fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()> {
446            let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
447                ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
448            })?;
449
450            let value = args.get_scalar("value").unwrap_or("default");
451            ctx.state.push_str(value);
452            Ok(())
453        }
454    }
455
456    // ============================================================================
457    // BASIC FUNCTIONALITY TESTS
458    // ============================================================================
459
460    #[test]
461    fn test_basic_execution() {
462        let handler = HelloCommand;
463        let mut context = TestContext::default();
464        let args = scalar_args([("name", "Rust")]);
465
466        let result = handler.execute(&mut context, &args);
467
468        assert!(result.is_ok());
469        assert_eq!(context.state, "Hello, Rust!");
470    }
471
472    #[test]
473    fn test_execution_without_args() {
474        let handler = HelloCommand;
475        let mut context = TestContext::default();
476        let args = ParsedArgs::from_scalars(HashMap::new());
477
478        let result = handler.execute(&mut context, &args);
479
480        assert!(result.is_ok());
481        assert_eq!(context.state, "Hello, World!");
482    }
483
484    #[test]
485    fn test_execution_with_empty_name() {
486        let handler = HelloCommand;
487        let mut context = TestContext::default();
488        let args = scalar_args([("name", "")]);
489
490        let result = handler.execute(&mut context, &args);
491
492        assert!(result.is_ok());
493        assert_eq!(context.state, "Hello, !");
494    }
495
496    // ============================================================================
497    // VALIDATION TESTS
498    // ============================================================================
499
500    #[test]
501    fn test_default_validation_accepts_all() {
502        let handler = HelloCommand;
503        let args = scalar_args([("random", "value")]);
504
505        let result = handler.validate(&args);
506
507        assert!(result.is_ok());
508    }
509
510    #[test]
511    fn test_custom_validation_success() {
512        let handler = ValidatedCommand;
513        let args = scalar_args([("count", "5")]);
514
515        let result = handler.validate(&args);
516
517        assert!(result.is_ok());
518    }
519
520    #[test]
521    fn test_custom_validation_missing_arg() {
522        let handler = ValidatedCommand;
523        let args = ParsedArgs::from_scalars(HashMap::new());
524
525        let result = handler.validate(&args);
526
527        assert!(result.is_err());
528        let err_msg = format!("{}", result.unwrap_err());
529        assert!(err_msg.contains("required"));
530    }
531
532    #[test]
533    fn test_custom_validation_invalid_value() {
534        let handler = ValidatedCommand;
535        let args = scalar_args([("count", "0")]);
536
537        let result = handler.validate(&args);
538
539        assert!(result.is_err());
540        let err_msg = format!("{}", result.unwrap_err());
541        assert!(err_msg.contains("positive"));
542    }
543
544    #[test]
545    fn test_custom_validation_non_integer() {
546        let handler = ValidatedCommand;
547        let args = scalar_args([("count", "abc")]);
548
549        let result = handler.validate(&args);
550
551        assert!(result.is_err());
552        let err_msg = format!("{}", result.unwrap_err());
553        assert!(err_msg.contains("integer"));
554    }
555
556    // ============================================================================
557    // ERROR HANDLING TESTS
558    // ============================================================================
559
560    #[test]
561    fn test_execution_failure() {
562        let handler = FailingCommand;
563        let mut context = TestContext::default();
564        let args = ParsedArgs::from_scalars(HashMap::new());
565
566        let result = handler.execute(&mut context, &args);
567
568        assert!(result.is_err());
569        let err_msg = format!("{}", result.unwrap_err());
570        assert!(err_msg.contains("Simulated failure"));
571    }
572
573    #[test]
574    fn test_context_downcast_failure() {
575        // Use a different context type to trigger downcast failure
576        struct WrongContext;
577
578        impl ExecutionContext for WrongContext {
579            fn as_any(&self) -> &dyn Any {
580                self
581            }
582
583            fn as_any_mut(&mut self) -> &mut dyn Any {
584                self
585            }
586        }
587
588        let handler = HelloCommand;
589        let mut wrong_context = WrongContext;
590        let args = ParsedArgs::from_scalars(HashMap::new());
591
592        let result = handler.execute(&mut wrong_context, &args);
593
594        assert!(result.is_err());
595        let err_msg = format!("{}", result.unwrap_err());
596        assert!(err_msg.contains("Wrong context type"));
597    }
598
599    // ============================================================================
600    // STATE MODIFICATION TESTS
601    // ============================================================================
602
603    #[test]
604    fn test_context_state_modification() {
605        let handler = StatefulCommand;
606        let mut context = TestContext {
607            state: "initial".to_string(),
608        };
609        let args = scalar_args([("value", "_modified")]);
610
611        let result = handler.execute(&mut context, &args);
612
613        assert!(result.is_ok());
614        assert_eq!(context.state, "initial_modified");
615    }
616
617    #[test]
618    fn test_multiple_executions_preserve_state() {
619        let handler = StatefulCommand;
620        let mut context = TestContext::default();
621
622        // First execution
623        let args1 = scalar_args([("value", "first")]);
624        handler.execute(&mut context, &args1).unwrap();
625        assert_eq!(context.state, "first");
626
627        // Second execution
628        let args2 = scalar_args([("value", "_second")]);
629        handler.execute(&mut context, &args2).unwrap();
630        assert_eq!(context.state, "first_second");
631    }
632
633    // ============================================================================
634    // TRAIT OBJECT TESTS
635    // ============================================================================
636
637    #[test]
638    fn test_trait_object_usage() {
639        // Verify that CommandHandler can be used as a trait object
640        let handler: Box<dyn CommandHandler> = Box::new(HelloCommand);
641        let mut context = TestContext::default();
642        let args = scalar_args([("name", "TraitObject")]);
643
644        let result = handler.execute(&mut context, &args);
645
646        assert!(result.is_ok());
647        assert_eq!(context.state, "Hello, TraitObject!");
648    }
649
650    #[test]
651    fn test_multiple_trait_objects() {
652        // Store multiple handlers as trait objects
653        let handlers: Vec<Box<dyn CommandHandler>> =
654            vec![Box::new(HelloCommand), Box::new(StatefulCommand)];
655
656        let mut context = TestContext::default();
657
658        // Execute first handler
659        let args1 = scalar_args([("name", "First")]);
660        handlers[0].execute(&mut context, &args1).unwrap();
661        assert_eq!(context.state, "Hello, First!");
662
663        // Execute second handler
664        context.state.clear();
665        let args2 = scalar_args([("value", "Second")]);
666        handlers[1].execute(&mut context, &args2).unwrap();
667        assert_eq!(context.state, "Second");
668    }
669
670    // ============================================================================
671    // THREAD SAFETY TESTS
672    // ============================================================================
673
674    #[test]
675    fn test_send_sync_requirement() {
676        // This test verifies that CommandHandler is Send + Sync
677        // by using it in a multi-threaded context
678        let handler: Arc<dyn CommandHandler> = Arc::new(HelloCommand);
679
680        // Clone the Arc to simulate sharing across threads
681        let handler_clone = handler.clone();
682
683        // This compilation test ensures Send + Sync are satisfied
684        let _ = std::thread::spawn(move || {
685            let _h = handler_clone;
686        });
687    }
688
689    #[test]
690    fn test_concurrent_validation() {
691        // Test that validation can be called from multiple threads
692        let handler = Arc::new(ValidatedCommand);
693        let handler_clone = handler.clone();
694
695        let handle = std::thread::spawn(move || {
696            let args = scalar_args([("count", "10")]);
697            handler_clone.validate(&args)
698        });
699
700        let args = scalar_args([("count", "5")]);
701        let result1 = handler.validate(&args);
702
703        let result2 = handle.join().unwrap();
704
705        assert!(result1.is_ok());
706        assert!(result2.is_ok());
707    }
708
709    // ============================================================================
710    // EDGE CASES
711    // ============================================================================
712
713    #[test]
714    fn test_empty_args() {
715        let handler = StatefulCommand;
716        let mut context = TestContext::default();
717        let args = ParsedArgs::from_scalars(HashMap::new());
718
719        // Should use default value
720        let result = handler.execute(&mut context, &args);
721
722        assert!(result.is_ok());
723        assert_eq!(context.state, "default");
724    }
725
726    #[test]
727    fn test_args_with_special_characters() {
728        let handler = HelloCommand;
729        let mut context = TestContext::default();
730        let args = scalar_args([("name", "Hello, δΈ–η•Œ! 🌍")]);
731
732        let result = handler.execute(&mut context, &args);
733
734        assert!(result.is_ok());
735        assert_eq!(context.state, "Hello, Hello, δΈ–η•Œ! 🌍!");
736    }
737
738    #[test]
739    fn test_very_long_argument() {
740        let handler = HelloCommand;
741        let mut context = TestContext::default();
742        let long_name = "x".repeat(10000);
743        let args = scalar_args([("name", long_name.as_str())]);
744
745        let result = handler.execute(&mut context, &args);
746
747        assert!(result.is_ok());
748        assert!(context.state.contains(&long_name));
749    }
750
751    // ============================================================================
752    // SHARED STATE TESTS
753    // ============================================================================
754
755    #[test]
756    fn test_shared_mutable_context() {
757        // Test that context can be safely modified by multiple commands
758        let handler1 = StatefulCommand;
759        let handler2 = StatefulCommand;
760        let mut context = TestContext::default();
761
762        let args1 = scalar_args([("value", "A")]);
763        handler1.execute(&mut context, &args1).unwrap();
764
765        let args2 = scalar_args([("value", "B")]);
766        handler2.execute(&mut context, &args2).unwrap();
767
768        assert_eq!(context.state, "AB");
769    }
770
771    // Test to ensure the trait is indeed object-safe at compile time
772    #[test]
773    fn test_object_safety_compile_time() {
774        // This function signature requires CommandHandler to be object-safe
775        fn _accepts_trait_object(_: &dyn CommandHandler) {}
776
777        // If this compiles, the trait is object-safe
778        let handler = HelloCommand;
779        _accepts_trait_object(&handler);
780    }
781
782    // Test that demonstrates why we can't have generic methods
783    // (This is a documentation test, not an actual test that runs)
784    /// ```compile_fail
785    /// use dynamic_cli::executor::CommandHandler;
786    ///
787    /// trait BrokenHandler: CommandHandler {
788    ///     fn generic_method<T>(&self, value: T);
789    /// }
790    ///
791    /// // This would fail because trait objects can't have generic methods
792    /// fn use_as_trait_object(handler: &dyn BrokenHandler) {
793    ///     // Cannot call generic_method on trait object
794    /// }
795    /// ```
796    #[allow(dead_code)]
797    fn test_no_generic_methods_documentation() {}
798
799    // ============================================================================
800    // AsyncCommandHandler TESTS (DD-022)
801    // ============================================================================
802
803    /// Async command that writes to the test context, mirroring `HelloCommand`.
804    struct AsyncHelloCommand;
805
806    #[async_trait]
807    impl AsyncCommandHandler for AsyncHelloCommand {
808        async fn execute(
809            &self,
810            context: &mut dyn ExecutionContext,
811            args: &ParsedArgs,
812        ) -> Result<()> {
813            let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
814                ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
815            })?;
816            let name = args.get_scalar("name").unwrap_or("World");
817            ctx.state = format!("Hello, {}!", name);
818            Ok(())
819        }
820    }
821
822    /// Async command with custom validation, mirroring `ValidatedCommand`.
823    struct AsyncValidatedCommand;
824
825    #[async_trait]
826    impl AsyncCommandHandler for AsyncValidatedCommand {
827        async fn execute(
828            &self,
829            _context: &mut dyn ExecutionContext,
830            _args: &ParsedArgs,
831        ) -> Result<()> {
832            Ok(())
833        }
834
835        async fn validate(&self, args: &ParsedArgs) -> Result<()> {
836            if args.get_scalar("count").is_none() {
837                return Err(
838                    ExecutionError::CommandFailed(anyhow::anyhow!("count is required")).into(),
839                );
840            }
841            Ok(())
842        }
843    }
844
845    /// Async command that fails during execution, mirroring `FailingCommand`.
846    struct AsyncFailingCommand;
847
848    #[async_trait]
849    impl AsyncCommandHandler for AsyncFailingCommand {
850        async fn execute(
851            &self,
852            _context: &mut dyn ExecutionContext,
853            _args: &ParsedArgs,
854        ) -> Result<()> {
855            Err(ExecutionError::CommandFailed(anyhow::anyhow!("Simulated async failure")).into())
856        }
857    }
858
859    #[test]
860    fn test_async_basic_execution() {
861        let handler = AsyncHelloCommand;
862        let mut context = TestContext::default();
863        let args = scalar_args([("name", "Rust")]);
864
865        let result = futures::executor::block_on(handler.execute(&mut context, &args));
866
867        assert!(result.is_ok());
868        assert_eq!(context.state, "Hello, Rust!");
869    }
870
871    #[test]
872    fn test_async_default_validation_accepts_all() {
873        let handler = AsyncHelloCommand;
874        let args = scalar_args([("random", "value")]);
875
876        let result = futures::executor::block_on(handler.validate(&args));
877
878        assert!(result.is_ok());
879    }
880
881    #[test]
882    fn test_async_custom_validation_missing_arg() {
883        let handler = AsyncValidatedCommand;
884        let args = ParsedArgs::from_scalars(HashMap::new());
885
886        let result = futures::executor::block_on(handler.validate(&args));
887
888        assert!(result.is_err());
889        let err_msg = format!("{}", result.unwrap_err());
890        assert!(err_msg.contains("required"));
891    }
892
893    #[test]
894    fn test_async_custom_validation_success() {
895        let handler = AsyncValidatedCommand;
896        let args = scalar_args([("count", "5")]);
897
898        let result = futures::executor::block_on(handler.validate(&args));
899
900        assert!(result.is_ok());
901    }
902
903    #[test]
904    fn test_async_execution_failure() {
905        let handler = AsyncFailingCommand;
906        let mut context = TestContext::default();
907        let args = ParsedArgs::from_scalars(HashMap::new());
908
909        let result = futures::executor::block_on(handler.execute(&mut context, &args));
910
911        assert!(result.is_err());
912        let err_msg = format!("{}", result.unwrap_err());
913        assert!(err_msg.contains("Simulated async failure"));
914    }
915
916    #[test]
917    fn test_async_trait_object_usage() {
918        // Verify that AsyncCommandHandler can be used as a trait object β€”
919        // the core object-safety guarantee DD-022 depends on.
920        let handler: Box<dyn AsyncCommandHandler> = Box::new(AsyncHelloCommand);
921        let mut context = TestContext::default();
922        let args = scalar_args([("name", "TraitObject")]);
923
924        let result = futures::executor::block_on(handler.execute(&mut context, &args));
925
926        assert!(result.is_ok());
927        assert_eq!(context.state, "Hello, TraitObject!");
928    }
929
930    #[test]
931    fn test_async_send_sync_requirement() {
932        // Verifies AsyncCommandHandler is Send + Sync by sharing it across
933        // threads via Arc β€” same pattern as test_send_sync_requirement above.
934        let handler: Arc<dyn AsyncCommandHandler> = Arc::new(AsyncHelloCommand);
935        let handler_clone = handler.clone();
936
937        let _ = std::thread::spawn(move || {
938            let _h = handler_clone;
939        });
940    }
941
942    #[test]
943    fn test_async_object_safety_compile_time() {
944        // If this compiles, AsyncCommandHandler is dyn-compatible.
945        fn _accepts_trait_object(_: &dyn AsyncCommandHandler) {}
946        _accepts_trait_object(&AsyncHelloCommand);
947    }
948}