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 `HashMap<String, String>` rather than generic types.
22//! This design choice:
23//! - Maintains object safety
24//! - Provides flexibility in argument types
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 std::collections::HashMap;
38//! use dynamic_cli::executor::CommandHandler;
39//! use dynamic_cli::context::ExecutionContext;
40//! use dynamic_cli::Result;
41//!
42//! // Define a simple command handler
43//! struct HelloCommand;
44//!
45//! impl CommandHandler for HelloCommand {
46//!     fn execute(
47//!         &self,
48//!         _context: &mut dyn ExecutionContext,
49//!         args: &HashMap<String, String>,
50//!     ) -> Result<()> {
51//!         let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
52//!         println!("Hello, {}!", name);
53//!         Ok(())
54//!     }
55//! }
56//! ```
57
58use crate::context::ExecutionContext;
59use crate::error::Result;
60use std::collections::HashMap;
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 `HashMap<String, String>`
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 std::collections::HashMap;
92/// use dynamic_cli::error::ExecutionError;
93/// use dynamic_cli::executor::CommandHandler;
94/// use dynamic_cli::context::ExecutionContext;
95/// use dynamic_cli::Result;
96///
97/// struct GreetCommand;
98///
99/// impl CommandHandler for GreetCommand {
100///     fn execute(
101///         &self,
102///         _context: &mut dyn ExecutionContext,
103///         args: &HashMap<String, String>,
104///     ) -> Result<()> {
105///         let name = args.get("name")
106///             .ok_or_else(|| {
107///                 ExecutionError::CommandFailed(
108///                     anyhow::anyhow!("Missing 'name' argument")
109///              )
110///          })?;
111///         
112///         let greeting = if let Some(formal) = args.get("formal") {
113///             if formal == "true" {
114///                 format!("Good day, {}.", name)
115///             } else {
116///                 format!("Hi, {}!", name)
117///             }
118///         } else {
119///             format!("Hello, {}!", name)
120///         };
121///         
122///         println!("{}", greeting);
123///         Ok(())
124///     }
125///     
126///     fn validate(&self, args: &HashMap<String, String>) -> Result<()> {
127///         // Custom validation: name must not be empty
128///         if let Some(name) = args.get("name") {
129///             if name.trim().is_empty() {
130///                 return Err(ExecutionError::CommandFailed(
131///                         anyhow::anyhow!("Name cannot be empty")
132///                 ).into());
133///             }
134///         }
135///         Ok(())
136///     }
137/// }
138/// ```
139pub trait CommandHandler: Send + Sync {
140    /// Execute the command with the given context and arguments
141    ///
142    /// This is the main entry point for command execution. It receives:
143    /// - A mutable reference to the execution context (for shared state)
144    /// - A map of argument names to their string values
145    ///
146    /// # Arguments
147    ///
148    /// * `context` - Mutable execution context for sharing state between commands.
149    ///               Use `downcast_ref` or `downcast_mut` from the `context` module
150    ///               to access your specific context type.
151    ///
152    /// * `args` - Parsed and validated arguments as name-value pairs.
153    ///           All values are strings; type conversion should be done
154    ///           within the handler if needed.
155    ///
156    /// # Returns
157    ///
158    /// - `Ok(())` if execution succeeds
159    /// - `Err(DynamicCliError)` if execution fails
160    ///
161    /// # Errors
162    ///
163    /// Implementations should return errors for:
164    /// - Invalid argument values (caught by validate, but can be rechecked)
165    /// - Execution failures (I/O errors, computation errors, etc.)
166    /// - Invalid context state
167    ///
168    /// Use `ExecutionError::CommandFailed` to wrap application-specific errors:
169    /// ```ignore
170    /// Err(ExecutionError::CommandFailed(anyhow::anyhow!("Details")).into())
171    /// ```
172    ///
173    /// # Example
174    ///
175    /// ```
176    /// # use std::collections::HashMap;
177    /// # use dynamic_cli::error::ExecutionError;
178    /// # use dynamic_cli::executor::CommandHandler;
179    /// # use dynamic_cli::context::ExecutionContext;
180    /// # use dynamic_cli::Result;
181    /// #
182    /// struct FileCommand;
183    ///
184    /// impl CommandHandler for FileCommand {
185    ///     fn execute(
186    ///         &self,
187    ///         _context: &mut dyn ExecutionContext,
188    ///         args: &HashMap<String, String>,
189    ///     ) -> Result<()> {
190    ///         let path = args.get("path")
191    ///             .ok_or_else(|| {
192    ///                ExecutionError::CommandFailed(
193    ///                       anyhow::anyhow!("Missing path argument")
194    ///             )
195    ///          })?;
196    ///         
197    ///         // Perform the actual work
198    ///         let content = std::fs::read_to_string(path)
199    ///             .map_err(|e| {
200    ///                ExecutionError::CommandFailed(anyhow::anyhow!("Failed to read file: {}", e))
201    ///         })?;
202    ///         
203    ///         println!("File contains {} bytes", content.len());
204    ///         Ok(())
205    ///     }
206    /// }
207    /// ```
208    fn execute(
209        &self,
210        context: &mut dyn ExecutionContext,
211        args: &HashMap<String, String>,
212    ) -> Result<()>;
213
214    /// Optional custom validation for arguments
215    ///
216    /// This method is called after the standard validation (type checking,
217    /// required arguments, etc.) but before execution. It allows commands
218    /// to implement custom validation logic.
219    ///
220    /// # Default Implementation
221    ///
222    /// The default implementation accepts all arguments (returns `Ok(())`).
223    /// Override this method only if you need custom validation.
224    ///
225    /// # Arguments
226    ///
227    /// * `args` - The arguments to validate
228    ///
229    /// # Returns
230    ///
231    /// - `Ok(())` if validation succeeds
232    /// - `Err(DynamicCliError)` if validation fails
233    ///
234    /// # Example
235    ///
236    /// ```
237    /// # use std::collections::HashMap;
238    /// # use dynamic_cli::executor::CommandHandler;
239    /// # use dynamic_cli::context::ExecutionContext;
240    /// # use dynamic_cli::error::ExecutionError;
241    /// # use dynamic_cli::Result;
242    /// #
243    /// struct RangeCommand;
244    ///
245    /// impl CommandHandler for RangeCommand {
246    ///     fn execute(
247    ///         &self,
248    ///         _context: &mut dyn ExecutionContext,
249    ///         args: &HashMap<String, String>,
250    ///     ) -> Result<()> {
251    ///         // Execution logic here
252    ///         Ok(())
253    ///     }
254    ///     
255    ///     fn validate(&self, args: &HashMap<String, String>) -> Result<()> {
256    ///         // Custom validation: ensure min < max
257    ///         if let (Some(min), Some(max)) = (args.get("min"), args.get("max")) {
258    ///             let min_val: f64 = min.parse()
259    ///                 .map_err(|_| {
260    ///                     ExecutionError::CommandFailed(anyhow::anyhow!("Invalid min value"))
261    ///             })?;
262    ///             let max_val: f64 = max.parse()
263    ///                 .map_err(|_| {ExecutionError::CommandFailed(anyhow::anyhow!("Invalid max value"))})?;
264    ///             
265    ///             if min_val >= max_val {
266    ///                 return Err(ExecutionError::CommandFailed(anyhow::anyhow!("min must be less than max")).into());
267    ///             }
268    ///         }
269    ///         Ok(())
270    ///     }
271    /// }
272    /// ```
273    fn validate(&self, _args: &HashMap<String, String>) -> Result<()> {
274        Ok(())
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use crate::error::ExecutionError;
282    use std::any::Any;
283    use std::sync::{Arc, Mutex};
284
285    // ============================================================================
286    // TEST FIXTURES
287    // ============================================================================
288
289    /// Simple test context for unit tests
290    #[derive(Default)]
291    struct TestContext {
292        state: String,
293    }
294
295    impl ExecutionContext for TestContext {
296        fn as_any(&self) -> &dyn Any {
297            self
298        }
299
300        fn as_any_mut(&mut self) -> &mut dyn Any {
301            self
302        }
303    }
304
305    /// Simple command that prints to context
306    struct HelloCommand;
307
308    impl CommandHandler for HelloCommand {
309        fn execute(
310            &self,
311            context: &mut dyn ExecutionContext,
312            args: &HashMap<String, String>,
313        ) -> Result<()> {
314            let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
315                ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
316            })?;
317
318            let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
319            ctx.state = format!("Hello, {}!", name);
320            Ok(())
321        }
322    }
323
324    /// Command with custom validation
325    struct ValidatedCommand;
326
327    impl CommandHandler for ValidatedCommand {
328        fn execute(
329            &self,
330            _context: &mut dyn ExecutionContext,
331            _args: &HashMap<String, String>,
332        ) -> Result<()> {
333            Ok(())
334        }
335
336        fn validate(&self, args: &HashMap<String, String>) -> Result<()> {
337            // Require "count" argument to be present and > 0
338            if let Some(count) = args.get("count") {
339                let count_val: i32 = count.parse().map_err(|_| {
340                    ExecutionError::CommandFailed(anyhow::anyhow!("count must be an integer"))
341                })?;
342
343                if count_val <= 0 {
344                    return Err(ExecutionError::CommandFailed(anyhow::anyhow!(
345                        "count must be positive"
346                    ))
347                    .into());
348                }
349            } else {
350                return Err(
351                    ExecutionError::CommandFailed(anyhow::anyhow!("count is required")).into(),
352                );
353            }
354            Ok(())
355        }
356    }
357
358    /// Command that fails during execution
359    struct FailingCommand;
360
361    impl CommandHandler for FailingCommand {
362        fn execute(
363            &self,
364            _context: &mut dyn ExecutionContext,
365            _args: &HashMap<String, String>,
366        ) -> Result<()> {
367            Err(ExecutionError::CommandFailed(anyhow::anyhow!("Simulated failure")).into())
368        }
369    }
370
371    /// Command that modifies context
372    struct StatefulCommand;
373
374    impl CommandHandler for StatefulCommand {
375        fn execute(
376            &self,
377            context: &mut dyn ExecutionContext,
378            args: &HashMap<String, String>,
379        ) -> Result<()> {
380            let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
381                ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
382            })?;
383
384            let value = args.get("value").map(|s| s.as_str()).unwrap_or("default");
385            ctx.state.push_str(value);
386            Ok(())
387        }
388    }
389
390    // ============================================================================
391    // BASIC FUNCTIONALITY TESTS
392    // ============================================================================
393
394    #[test]
395    fn test_basic_execution() {
396        let handler = HelloCommand;
397        let mut context = TestContext::default();
398        let mut args = HashMap::new();
399        args.insert("name".to_string(), "Rust".to_string());
400
401        let result = handler.execute(&mut context, &args);
402
403        assert!(result.is_ok());
404        assert_eq!(context.state, "Hello, Rust!");
405    }
406
407    #[test]
408    fn test_execution_without_args() {
409        let handler = HelloCommand;
410        let mut context = TestContext::default();
411        let args = HashMap::new();
412
413        let result = handler.execute(&mut context, &args);
414
415        assert!(result.is_ok());
416        assert_eq!(context.state, "Hello, World!");
417    }
418
419    #[test]
420    fn test_execution_with_empty_name() {
421        let handler = HelloCommand;
422        let mut context = TestContext::default();
423        let mut args = HashMap::new();
424        args.insert("name".to_string(), "".to_string());
425
426        let result = handler.execute(&mut context, &args);
427
428        assert!(result.is_ok());
429        assert_eq!(context.state, "Hello, !");
430    }
431
432    // ============================================================================
433    // VALIDATION TESTS
434    // ============================================================================
435
436    #[test]
437    fn test_default_validation_accepts_all() {
438        let handler = HelloCommand;
439        let mut args = HashMap::new();
440        args.insert("random".to_string(), "value".to_string());
441
442        let result = handler.validate(&args);
443
444        assert!(result.is_ok());
445    }
446
447    #[test]
448    fn test_custom_validation_success() {
449        let handler = ValidatedCommand;
450        let mut args = HashMap::new();
451        args.insert("count".to_string(), "5".to_string());
452
453        let result = handler.validate(&args);
454
455        assert!(result.is_ok());
456    }
457
458    #[test]
459    fn test_custom_validation_missing_arg() {
460        let handler = ValidatedCommand;
461        let args = HashMap::new();
462
463        let result = handler.validate(&args);
464
465        assert!(result.is_err());
466        let err_msg = format!("{}", result.unwrap_err());
467        assert!(err_msg.contains("required"));
468    }
469
470    #[test]
471    fn test_custom_validation_invalid_value() {
472        let handler = ValidatedCommand;
473        let mut args = HashMap::new();
474        args.insert("count".to_string(), "0".to_string());
475
476        let result = handler.validate(&args);
477
478        assert!(result.is_err());
479        let err_msg = format!("{}", result.unwrap_err());
480        assert!(err_msg.contains("positive"));
481    }
482
483    #[test]
484    fn test_custom_validation_non_integer() {
485        let handler = ValidatedCommand;
486        let mut args = HashMap::new();
487        args.insert("count".to_string(), "abc".to_string());
488
489        let result = handler.validate(&args);
490
491        assert!(result.is_err());
492        let err_msg = format!("{}", result.unwrap_err());
493        assert!(err_msg.contains("integer"));
494    }
495
496    // ============================================================================
497    // ERROR HANDLING TESTS
498    // ============================================================================
499
500    #[test]
501    fn test_execution_failure() {
502        let handler = FailingCommand;
503        let mut context = TestContext::default();
504        let args = HashMap::new();
505
506        let result = handler.execute(&mut context, &args);
507
508        assert!(result.is_err());
509        let err_msg = format!("{}", result.unwrap_err());
510        assert!(err_msg.contains("Simulated failure"));
511    }
512
513    #[test]
514    fn test_context_downcast_failure() {
515        // Use a different context type to trigger downcast failure
516        #[derive(Default)]
517        struct WrongContext;
518
519        impl ExecutionContext for WrongContext {
520            fn as_any(&self) -> &dyn Any {
521                self
522            }
523
524            fn as_any_mut(&mut self) -> &mut dyn Any {
525                self
526            }
527        }
528
529        let handler = HelloCommand;
530        let mut wrong_context = WrongContext::default();
531        let args = HashMap::new();
532
533        let result = handler.execute(&mut wrong_context, &args);
534
535        assert!(result.is_err());
536        let err_msg = format!("{}", result.unwrap_err());
537        assert!(err_msg.contains("Wrong context type"));
538    }
539
540    // ============================================================================
541    // STATE MODIFICATION TESTS
542    // ============================================================================
543
544    #[test]
545    fn test_context_state_modification() {
546        let handler = StatefulCommand;
547        let mut context = TestContext::default();
548        context.state = "initial".to_string();
549        let mut args = HashMap::new();
550        args.insert("value".to_string(), "_modified".to_string());
551
552        let result = handler.execute(&mut context, &args);
553
554        assert!(result.is_ok());
555        assert_eq!(context.state, "initial_modified");
556    }
557
558    #[test]
559    fn test_multiple_executions_preserve_state() {
560        let handler = StatefulCommand;
561        let mut context = TestContext::default();
562
563        // First execution
564        let mut args1 = HashMap::new();
565        args1.insert("value".to_string(), "first".to_string());
566        handler.execute(&mut context, &args1).unwrap();
567        assert_eq!(context.state, "first");
568
569        // Second execution
570        let mut args2 = HashMap::new();
571        args2.insert("value".to_string(), "_second".to_string());
572        handler.execute(&mut context, &args2).unwrap();
573        assert_eq!(context.state, "first_second");
574    }
575
576    // ============================================================================
577    // TRAIT OBJECT TESTS
578    // ============================================================================
579
580    #[test]
581    fn test_trait_object_usage() {
582        // Verify that CommandHandler can be used as a trait object
583        let handler: Box<dyn CommandHandler> = Box::new(HelloCommand);
584        let mut context = TestContext::default();
585        let mut args = HashMap::new();
586        args.insert("name".to_string(), "TraitObject".to_string());
587
588        let result = handler.execute(&mut context, &args);
589
590        assert!(result.is_ok());
591        assert_eq!(context.state, "Hello, TraitObject!");
592    }
593
594    #[test]
595    fn test_multiple_trait_objects() {
596        // Store multiple handlers as trait objects
597        let handlers: Vec<Box<dyn CommandHandler>> =
598            vec![Box::new(HelloCommand), Box::new(StatefulCommand)];
599
600        let mut context = TestContext::default();
601
602        // Execute first handler
603        let mut args1 = HashMap::new();
604        args1.insert("name".to_string(), "First".to_string());
605        handlers[0].execute(&mut context, &args1).unwrap();
606        assert_eq!(context.state, "Hello, First!");
607
608        // Execute second handler
609        context.state.clear();
610        let mut args2 = HashMap::new();
611        args2.insert("value".to_string(), "Second".to_string());
612        handlers[1].execute(&mut context, &args2).unwrap();
613        assert_eq!(context.state, "Second");
614    }
615
616    // ============================================================================
617    // THREAD SAFETY TESTS
618    // ============================================================================
619
620    #[test]
621    fn test_send_sync_requirement() {
622        // This test verifies that CommandHandler is Send + Sync
623        // by using it in a multi-threaded context
624        let handler: Arc<dyn CommandHandler> = Arc::new(HelloCommand);
625
626        // Clone the Arc to simulate sharing across threads
627        let handler_clone = handler.clone();
628
629        // This compilation test ensures Send + Sync are satisfied
630        let _ = std::thread::spawn(move || {
631            let _h = handler_clone;
632        });
633    }
634
635    #[test]
636    fn test_concurrent_validation() {
637        // Test that validation can be called from multiple threads
638        let handler = Arc::new(ValidatedCommand);
639        let handler_clone = handler.clone();
640
641        let handle = std::thread::spawn(move || {
642            let mut args = HashMap::new();
643            args.insert("count".to_string(), "10".to_string());
644            handler_clone.validate(&args)
645        });
646
647        let mut args = HashMap::new();
648        args.insert("count".to_string(), "5".to_string());
649        let result1 = handler.validate(&args);
650
651        let result2 = handle.join().unwrap();
652
653        assert!(result1.is_ok());
654        assert!(result2.is_ok());
655    }
656
657    // ============================================================================
658    // EDGE CASES
659    // ============================================================================
660
661    #[test]
662    fn test_empty_args() {
663        let handler = StatefulCommand;
664        let mut context = TestContext::default();
665        let args = HashMap::new();
666
667        // Should use default value
668        let result = handler.execute(&mut context, &args);
669
670        assert!(result.is_ok());
671        assert_eq!(context.state, "default");
672    }
673
674    #[test]
675    fn test_args_with_special_characters() {
676        let handler = HelloCommand;
677        let mut context = TestContext::default();
678        let mut args = HashMap::new();
679        args.insert("name".to_string(), "Hello, δΈ–η•Œ! 🌍".to_string());
680
681        let result = handler.execute(&mut context, &args);
682
683        assert!(result.is_ok());
684        assert_eq!(context.state, "Hello, Hello, δΈ–η•Œ! 🌍!");
685    }
686
687    #[test]
688    fn test_very_long_argument() {
689        let handler = HelloCommand;
690        let mut context = TestContext::default();
691        let mut args = HashMap::new();
692        let long_name = "x".repeat(10000);
693        args.insert("name".to_string(), long_name.clone());
694
695        let result = handler.execute(&mut context, &args);
696
697        assert!(result.is_ok());
698        assert!(context.state.contains(&long_name));
699    }
700
701    // ============================================================================
702    // SHARED STATE TESTS
703    // ============================================================================
704
705    #[test]
706    fn test_shared_mutable_context() {
707        // Test that context can be safely modified by multiple commands
708        let handler1 = StatefulCommand;
709        let handler2 = StatefulCommand;
710        let mut context = TestContext::default();
711
712        let mut args1 = HashMap::new();
713        args1.insert("value".to_string(), "A".to_string());
714        handler1.execute(&mut context, &args1).unwrap();
715
716        let mut args2 = HashMap::new();
717        args2.insert("value".to_string(), "B".to_string());
718        handler2.execute(&mut context, &args2).unwrap();
719
720        assert_eq!(context.state, "AB");
721    }
722
723    // Test to ensure the trait is indeed object-safe at compile time
724    #[test]
725    fn test_object_safety_compile_time() {
726        // This function signature requires CommandHandler to be object-safe
727        fn _accepts_trait_object(_: &dyn CommandHandler) {}
728
729        // If this compiles, the trait is object-safe
730        let handler = HelloCommand;
731        _accepts_trait_object(&handler);
732    }
733
734    // Test that demonstrates why we can't have generic methods
735    // (This is a documentation test, not an actual test that runs)
736    /// ```compile_fail
737    /// use dynamic_cli::executor::CommandHandler;
738    ///
739    /// trait BrokenHandler: CommandHandler {
740    ///     fn generic_method<T>(&self, value: T);
741    /// }
742    ///
743    /// // This would fail because trait objects can't have generic methods
744    /// fn use_as_trait_object(handler: &dyn BrokenHandler) {
745    ///     // Cannot call generic_method on trait object
746    /// }
747    /// ```
748    #[allow(dead_code)]
749    fn test_no_generic_methods_documentation() {}
750}