dynamic_cli/executor/
mod.rs

1//! Command execution module
2//!
3//! This module provides the core functionality for executing commands in the
4//! dynamic-cli framework. It defines the [`CommandHandler`] trait that all
5//! command implementations must satisfy.
6//!
7//! # Module Organization
8//!
9//! - [`traits`]: Core trait definitions (`CommandHandler`)
10//! - `command_executor` (future): Executor logic for running commands
11//!
12//! # Architecture
13//!
14//! The execution flow in dynamic-cli follows this pattern:
15//!
16//! ```text
17//! User Input → Parser → Validator → Executor → Command Handler
18//!                                      ↓
19//!                                  Context
20//! ```
21//!
22//! 1. **Parser**: Converts raw input to structured arguments
23//! 2. **Validator**: Checks argument types and constraints
24//! 3. **Executor**: Looks up and invokes the appropriate handler
25//! 4. **Handler**: Executes the command logic with access to context
26//!
27//! # Design Philosophy
28//!
29//! ## Object Safety
30//!
31//! The module is designed around object-safe traits to enable dynamic dispatch.
32//! This allows:
33//! - Runtime registration of commands
34//! - Storing heterogeneous handlers in collections
35//! - Plugin-style architecture where handlers are loaded dynamically
36//!
37//! ## Thread Safety
38//!
39//! All types are `Send + Sync` to support:
40//! - Multi-threaded CLI applications
41//! - Concurrent command execution (future enhancement)
42//! - Safe shared access to the command registry
43//!
44//! ## Simplicity
45//!
46//! The API is intentionally kept simple:
47//! - Arguments are passed as `HashMap<String, String>`
48//! - Context is accessed through trait objects
49//! - Error handling uses the framework's standard `Result` type
50//!
51//! # Quick Start
52//!
53//! ```
54//! use std::collections::HashMap;
55//! use dynamic_cli::error::ExecutionError;
56//! use dynamic_cli::executor::CommandHandler;
57//! use dynamic_cli::context::ExecutionContext;
58//! use dynamic_cli::Result;
59//!
60//! // 1. Define your context
61//! #[derive(Default)]
62//! struct AppContext {
63//!     counter: i32,
64//! }
65//!
66//! impl ExecutionContext for AppContext {
67//!     fn as_any(&self) -> &dyn std::any::Any { self }
68//!     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
69//! }
70//!
71//! // 2. Implement a command handler
72//! struct IncrementCommand;
73//!
74//! impl CommandHandler for IncrementCommand {
75//!     fn execute(
76//!         &self,
77//!         context: &mut dyn ExecutionContext,
78//!         args: &HashMap<String, String>,
79//!     ) -> Result<()> {
80//!         let ctx = dynamic_cli::context::downcast_mut::<AppContext>(context)
81//!             .ok_or_else(|| ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type")))?;
82//!         
83//!         let amount: i32 = args.get("amount")
84//!             .and_then(|s| s.parse().ok())
85//!             .unwrap_or(1);
86//!         
87//!         ctx.counter += amount;
88//!         println!("Counter is now: {}", ctx.counter);
89//!         Ok(())
90//!     }
91//! }
92//!
93//! // 3. Use the handler
94//! # fn main() -> Result<()> {
95//! let handler = IncrementCommand;
96//! let mut context = AppContext::default();
97//! let mut args = HashMap::new();
98//! args.insert("amount".to_string(), "5".to_string());
99//!
100//! handler.execute(&mut context, &args)?;
101//! assert_eq!(context.counter, 5);
102//! # Ok(())
103//! # }
104//! ```
105//!
106//! # Examples
107//!
108//! ## Basic Command
109//!
110//! ```
111//! use std::collections::HashMap;
112//! use dynamic_cli::executor::CommandHandler;
113//! use dynamic_cli::context::ExecutionContext;
114//! use dynamic_cli::Result;
115//!
116//! struct EchoCommand;
117//!
118//! impl CommandHandler for EchoCommand {
119//!     fn execute(
120//!         &self,
121//!         _context: &mut dyn ExecutionContext,
122//!         args: &HashMap<String, String>,
123//!     ) -> Result<()> {
124//!         if let Some(message) = args.get("message") {
125//!             println!("{}", message);
126//!         }
127//!         Ok(())
128//!     }
129//! }
130//! ```
131//!
132//! ## Command with Validation
133//!
134//! ```
135//! use std::collections::HashMap;
136//! use dynamic_cli::error::ExecutionError;
137//! use dynamic_cli::executor::CommandHandler;
138//! use dynamic_cli::context::ExecutionContext;
139//! use dynamic_cli::DynamicCliError::Execution;
140//! use dynamic_cli::Result;
141//!
142//! struct DivideCommand;
143//!
144//! impl CommandHandler for DivideCommand {
145//!     fn execute(
146//!         &self,
147//!         _context: &mut dyn ExecutionContext,
148//!         args: &HashMap<String, String>,
149//!     ) -> dynamic_cli::Result<()> {
150//!         let denom = args.get("denominator")
151//!             .ok_or_else(|| {
152//!                 ExecutionError::CommandFailed(
153//!                     anyhow::anyhow!("Missing Denominator"))})?;
154//!
155//!         let value: f64 = denom.parse()
156//!             .map_err(|_| {
157//!                 ExecutionError::CommandFailed(
158//!                     anyhow::anyhow!("Invalid Denominator"))})?;
159//!
160//!         if value == 0.0 {
161//!             return Err(ExecutionError::CommandFailed(
162//!                     anyhow::anyhow!("Cannot divide by zero")).into());
163//!         }
164//!         Ok(())
165//!     }
166//! }
167//! ```
168//!
169//! ## Stateful Command
170//!
171//! ```
172//! use std::collections::HashMap;
173//! use dynamic_cli::error::ExecutionError;
174//! use dynamic_cli::executor::CommandHandler;
175//! use dynamic_cli::context::ExecutionContext;
176//! use dynamic_cli::Result;
177//!
178//! #[derive(Default)]
179//! struct FileContext {
180//!     current_file: Option<String>,
181//! }
182//!
183//! impl ExecutionContext for FileContext {
184//!     fn as_any(&self) -> &dyn std::any::Any { self }
185//!     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
186//! }
187//!
188//! struct OpenCommand;
189//!
190//! impl CommandHandler for OpenCommand {
191//!     fn execute(
192//!         &self,
193//!         context: &mut dyn ExecutionContext,
194//!         args: &HashMap<String, String>,
195//!     ) -> Result<()> {
196//!         let ctx = dynamic_cli::context::downcast_mut::<FileContext>(context)
197//!             .ok_or_else(|| ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type")))?;
198//!         
199//!         let filename = args.get("file")
200//!             .ok_or_else(|| { ExecutionError::CommandFailed(anyhow::anyhow!("Missing file argument"))})?;
201//!         
202//!         ctx.current_file = Some(filename.clone());
203//!         println!("Opened: {}", filename);
204//!         Ok(())
205//!     }
206//! }
207//! ```
208//!
209//! # Advanced Usage
210//!
211//! ## Dynamic Command Registration
212//!
213//! Commands can be registered dynamically at runtime using trait objects:
214//!
215//! ```
216//! use std::collections::HashMap;
217//! use dynamic_cli::executor::CommandHandler;
218//! # use dynamic_cli::context::ExecutionContext;
219//! # use dynamic_cli::Result;
220//!
221//! // Store commands in a registry
222//! struct CommandRegistry {
223//!     handlers: HashMap<String, Box<dyn CommandHandler>>,
224//! }
225//!
226//! impl CommandRegistry {
227//!     fn new() -> Self {
228//!         Self {
229//!             handlers: HashMap::new(),
230//!         }
231//!     }
232//!     
233//!     fn register(&mut self, name: String, handler: Box<dyn CommandHandler>) {
234//!         self.handlers.insert(name, handler);
235//!     }
236//!     
237//!     fn get(&self, name: &str) -> Option<&Box<dyn CommandHandler>> {
238//!         self.handlers.get(name)
239//!     }
240//! }
241//! ```
242//!
243//! ## Error Handling Pattern
244//!
245//! ```
246//! use std::collections::HashMap;
247//! use dynamic_cli::executor::CommandHandler;
248//! use dynamic_cli::context::ExecutionContext;
249//! use dynamic_cli::error::ExecutionError;
250//! use dynamic_cli::Result;
251//!
252//! struct FileCommand;
253//!
254//! impl CommandHandler for FileCommand {
255//!     fn execute(
256//!         &self,
257//!         _context: &mut dyn ExecutionContext,
258//!         args: &HashMap<String, String>,
259//!     ) -> Result<()> {
260//!         let path = args.get("path")
261//!             .ok_or_else(|| { ExecutionError::CommandFailed(anyhow::anyhow!("Missing path argument"))})?;
262//!         
263//!         // Wrap application errors in ExecutionError
264//!         std::fs::read_to_string(path)
265//!             .map_err(|e| ExecutionError::CommandFailed(
266//!                 anyhow::anyhow!("Failed to read file: {}", e)
267//!             ))?;
268//!         
269//!         Ok(())
270//!     }
271//! }
272//! ```
273
274// Public submodules
275pub mod traits;
276
277// Public re-exports for convenience
278pub use traits::CommandHandler;
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::context::ExecutionContext;
284    use crate::error::ExecutionError;
285    use std::any::Any;
286    use std::collections::HashMap;
287
288    // ============================================================================
289    // INTEGRATION TEST FIXTURES
290    // ============================================================================
291
292    /// Test context for integration tests
293    #[derive(Default)]
294    struct IntegrationContext {
295        log: Vec<String>,
296        state: HashMap<String, String>,
297    }
298
299    impl ExecutionContext for IntegrationContext {
300        fn as_any(&self) -> &dyn Any {
301            self
302        }
303
304        fn as_any_mut(&mut self) -> &mut dyn Any {
305            self
306        }
307    }
308
309    /// Command that logs its execution
310    struct LogCommand;
311
312    impl CommandHandler for LogCommand {
313        fn execute(
314            &self,
315            context: &mut dyn ExecutionContext,
316            args: &HashMap<String, String>,
317        ) -> crate::error::Result<()> {
318            let ctx =
319                crate::context::downcast_mut::<IntegrationContext>(context).ok_or_else(|| {
320                    ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
321                })?;
322
323            let message = args
324                .get("message")
325                .map(|s| s.as_str())
326                .unwrap_or("default message");
327            ctx.log.push(message.to_string());
328            Ok(())
329        }
330    }
331
332    /// Command that sets state
333    struct SetCommand;
334
335    impl CommandHandler for SetCommand {
336        fn execute(
337            &self,
338            context: &mut dyn ExecutionContext,
339            args: &HashMap<String, String>,
340        ) -> crate::error::Result<()> {
341            let ctx =
342                crate::context::downcast_mut::<IntegrationContext>(context).ok_or_else(|| {
343                    ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
344                })?;
345
346            if let (Some(key), Some(value)) = (args.get("key"), args.get("value")) {
347                ctx.state.insert(key.clone(), value.clone());
348            }
349            Ok(())
350        }
351    }
352
353    /// Command that reads state
354    struct GetCommand;
355
356    impl CommandHandler for GetCommand {
357        fn execute(
358            &self,
359            context: &mut dyn ExecutionContext,
360            args: &HashMap<String, String>,
361        ) -> crate::error::Result<()> {
362            let ctx =
363                crate::context::downcast_mut::<IntegrationContext>(context).ok_or_else(|| {
364                    ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
365                })?;
366
367            if let Some(key) = args.get("key") {
368                if let Some(value) = ctx.state.get(key) {
369                    ctx.log.push(format!("{} = {}", key, value));
370                } else {
371                    ctx.log.push(format!("{} not found", key));
372                }
373            }
374            Ok(())
375        }
376    }
377
378    // ============================================================================
379    // INTEGRATION TESTS
380    // ============================================================================
381
382    #[test]
383    fn test_module_reexports() {
384        // Verify that CommandHandler is accessible from module root
385        fn _accepts_handler(_: &dyn CommandHandler) {}
386
387        let handler = LogCommand;
388        _accepts_handler(&handler);
389    }
390
391    #[test]
392    fn test_command_sequence() {
393        // Test executing multiple commands in sequence
394        let mut context = IntegrationContext::default();
395
396        // Execute first command
397        let log_cmd = LogCommand;
398        let mut args1 = HashMap::new();
399        args1.insert("message".to_string(), "First".to_string());
400        log_cmd.execute(&mut context, &args1).unwrap();
401
402        // Execute second command
403        let mut args2 = HashMap::new();
404        args2.insert("message".to_string(), "Second".to_string());
405        log_cmd.execute(&mut context, &args2).unwrap();
406
407        // Verify both commands executed
408        assert_eq!(context.log.len(), 2);
409        assert_eq!(context.log[0], "First");
410        assert_eq!(context.log[1], "Second");
411    }
412
413    #[test]
414    fn test_stateful_workflow() {
415        // Test a complete workflow with state management
416        let mut context = IntegrationContext::default();
417
418        // Set some values
419        let set_cmd = SetCommand;
420        let mut args1 = HashMap::new();
421        args1.insert("key".to_string(), "name".to_string());
422        args1.insert("value".to_string(), "Alice".to_string());
423        set_cmd.execute(&mut context, &args1).unwrap();
424
425        let mut args2 = HashMap::new();
426        args2.insert("key".to_string(), "age".to_string());
427        args2.insert("value".to_string(), "30".to_string());
428        set_cmd.execute(&mut context, &args2).unwrap();
429
430        // Retrieve values
431        let get_cmd = GetCommand;
432        let mut args3 = HashMap::new();
433        args3.insert("key".to_string(), "name".to_string());
434        get_cmd.execute(&mut context, &args3).unwrap();
435
436        let mut args4 = HashMap::new();
437        args4.insert("key".to_string(), "age".to_string());
438        get_cmd.execute(&mut context, &args4).unwrap();
439
440        // Verify workflow
441        assert_eq!(context.state.len(), 2);
442        assert_eq!(context.state.get("name"), Some(&"Alice".to_string()));
443        assert_eq!(context.state.get("age"), Some(&"30".to_string()));
444        assert_eq!(context.log.len(), 2);
445        assert_eq!(context.log[0], "name = Alice");
446        assert_eq!(context.log[1], "age = 30");
447    }
448
449    #[test]
450    fn test_heterogeneous_handler_collection() {
451        // Test storing different handler types in a collection
452        let handlers: Vec<Box<dyn CommandHandler>> = vec![
453            Box::new(LogCommand),
454            Box::new(SetCommand),
455            Box::new(GetCommand),
456        ];
457
458        // Verify we can store different handlers
459        assert_eq!(handlers.len(), 3);
460
461        // Execute each handler
462        let mut context = IntegrationContext::default();
463
464        let mut args1 = HashMap::new();
465        args1.insert("message".to_string(), "test".to_string());
466        handlers[0].execute(&mut context, &args1).unwrap();
467
468        let mut args2 = HashMap::new();
469        args2.insert("key".to_string(), "k".to_string());
470        args2.insert("value".to_string(), "v".to_string());
471        handlers[1].execute(&mut context, &args2).unwrap();
472
473        let mut args3 = HashMap::new();
474        args3.insert("key".to_string(), "k".to_string());
475        handlers[2].execute(&mut context, &args3).unwrap();
476
477        assert_eq!(context.log.len(), 2);
478        assert_eq!(context.state.len(), 1);
479    }
480
481    #[test]
482    fn test_context_isolation_between_commands() {
483        // Verify that context is shared correctly
484        let mut context = IntegrationContext::default();
485
486        let set_cmd = SetCommand;
487        let mut args = HashMap::new();
488        args.insert("key".to_string(), "shared".to_string());
489        args.insert("value".to_string(), "value".to_string());
490        set_cmd.execute(&mut context, &args).unwrap();
491
492        // Another command can see the state
493        let get_cmd = GetCommand;
494        let mut args2 = HashMap::new();
495        args2.insert("key".to_string(), "shared".to_string());
496        get_cmd.execute(&mut context, &args2).unwrap();
497
498        assert!(context.log[0].contains("shared = value"));
499    }
500
501    #[test]
502    fn test_error_propagation() {
503        // Test that errors propagate correctly through the handler chain
504        struct FailingCommand;
505
506        impl CommandHandler for FailingCommand {
507            fn execute(
508                &self,
509                _context: &mut dyn ExecutionContext,
510                _args: &HashMap<String, String>,
511            ) -> crate::error::Result<()> {
512                Err(ExecutionError::CommandFailed(anyhow::anyhow!("Intentional failure")).into())
513            }
514        }
515
516        let handler = FailingCommand;
517        let mut context = IntegrationContext::default();
518        let args = HashMap::new();
519
520        let result = handler.execute(&mut context, &args);
521
522        assert!(result.is_err());
523        let err_msg = format!("{}", result.unwrap_err());
524        assert!(err_msg.contains("Intentional failure"));
525    }
526
527    #[test]
528    fn test_validation_before_execution() {
529        // Test the intended workflow: validate then execute
530        struct ValidatingCommand;
531
532        impl CommandHandler for ValidatingCommand {
533            fn execute(
534                &self,
535                context: &mut dyn ExecutionContext,
536                _args: &HashMap<String, String>,
537            ) -> crate::error::Result<()> {
538                let ctx = crate::context::downcast_mut::<IntegrationContext>(context).ok_or_else(
539                    || ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type")),
540                )?;
541                ctx.log.push("executed".to_string());
542                Ok(())
543            }
544
545            fn validate(&self, args: &HashMap<String, String>) -> crate::error::Result<()> {
546                if !args.contains_key("required") {
547                    return Err(ExecutionError::CommandFailed(anyhow::anyhow!(
548                        "Missing required argument"
549                    ))
550                    .into());
551                }
552                Ok(())
553            }
554        }
555
556        let handler = ValidatingCommand;
557        let mut context = IntegrationContext::default();
558
559        // Test validation failure
560        let args_invalid = HashMap::new();
561        assert!(handler.validate(&args_invalid).is_err());
562        assert!(handler.execute(&mut context, &args_invalid).is_ok()); // Execute would work
563
564        // Test validation success
565        let mut args_valid = HashMap::new();
566        args_valid.insert("required".to_string(), "value".to_string());
567        assert!(handler.validate(&args_valid).is_ok());
568        assert!(handler.execute(&mut context, &args_valid).is_ok());
569    }
570
571    #[test]
572    fn test_command_handler_documentation_example() {
573        // Test the example from module documentation
574        #[derive(Default)]
575        struct AppContext {
576            counter: i32,
577        }
578
579        impl ExecutionContext for AppContext {
580            fn as_any(&self) -> &dyn Any {
581                self
582            }
583            fn as_any_mut(&mut self) -> &mut dyn Any {
584                self
585            }
586        }
587
588        struct IncrementCommand;
589
590        impl CommandHandler for IncrementCommand {
591            fn execute(
592                &self,
593                context: &mut dyn ExecutionContext,
594                args: &HashMap<String, String>,
595            ) -> crate::error::Result<()> {
596                let ctx = crate::context::downcast_mut::<AppContext>(context).ok_or_else(|| {
597                    ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
598                })?;
599
600                let amount: i32 = args.get("amount").and_then(|s| s.parse().ok()).unwrap_or(1);
601
602                ctx.counter += amount;
603                Ok(())
604            }
605        }
606
607        let handler = IncrementCommand;
608        let mut context = AppContext::default();
609        let mut args = HashMap::new();
610        args.insert("amount".to_string(), "5".to_string());
611
612        handler.execute(&mut context, &args).unwrap();
613        assert_eq!(context.counter, 5);
614    }
615
616    #[test]
617    fn test_complex_multi_step_workflow() {
618        // Test a more complex workflow simulating real usage
619        let mut context = IntegrationContext::default();
620
621        // Step 1: Initialize some state
622        let set_cmd = SetCommand;
623        let mut args1 = HashMap::new();
624        args1.insert("key".to_string(), "initialized".to_string());
625        args1.insert("value".to_string(), "true".to_string());
626        set_cmd.execute(&mut context, &args1).unwrap();
627
628        // Step 2: Log the initialization
629        let log_cmd = LogCommand;
630        let mut args2 = HashMap::new();
631        args2.insert("message".to_string(), "System initialized".to_string());
632        log_cmd.execute(&mut context, &args2).unwrap();
633
634        // Step 3: Set more state
635        let mut args3 = HashMap::new();
636        args3.insert("key".to_string(), "user".to_string());
637        args3.insert("value".to_string(), "admin".to_string());
638        set_cmd.execute(&mut context, &args3).unwrap();
639
640        // Step 4: Query state
641        let get_cmd = GetCommand;
642        let mut args4 = HashMap::new();
643        args4.insert("key".to_string(), "user".to_string());
644        get_cmd.execute(&mut context, &args4).unwrap();
645
646        // Verify the complete workflow
647        assert_eq!(context.state.len(), 2);
648        assert_eq!(context.log.len(), 2);
649        assert_eq!(context.log[0], "System initialized");
650        assert_eq!(context.log[1], "user = admin");
651    }
652}