Skip to main content

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 [`crate::parser::ParsedArgs`]
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 dynamic_cli::error::ExecutionError;
55//! use dynamic_cli::executor::{CommandHandler, ParsedArgs};
56//! use dynamic_cli::context::ExecutionContext;
57//! use dynamic_cli::Result;
58//! use std::collections::HashMap;
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: &ParsedArgs,
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_scalar("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 raw_args = HashMap::new();
98//! raw_args.insert("amount".to_string(), "5".to_string());
99//! let args = ParsedArgs::from_scalars(raw_args);
100//!
101//! handler.execute(&mut context, &args)?;
102//! assert_eq!(context.counter, 5);
103//! # Ok(())
104//! # }
105//! ```
106//!
107//! # Examples
108//!
109//! ## Basic Command
110//!
111//! ```
112//! use dynamic_cli::executor::{CommandHandler, ParsedArgs};
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: &ParsedArgs,
123//!     ) -> Result<()> {
124//!         if let Some(message) = args.get_scalar("message") {
125//!             println!("{}", message);
126//!         }
127//!         Ok(())
128//!     }
129//! }
130//! ```
131//!
132//! ## Command with Validation
133//!
134//! ```
135//! use dynamic_cli::error::ExecutionError;
136//! use dynamic_cli::executor::{CommandHandler, ParsedArgs};
137//! use dynamic_cli::context::ExecutionContext;
138//! use dynamic_cli::DynamicCliError::Execution;
139//! use dynamic_cli::Result;
140//!
141//! struct DivideCommand;
142//!
143//! impl CommandHandler for DivideCommand {
144//!     fn execute(
145//!         &self,
146//!         _context: &mut dyn ExecutionContext,
147//!         args: &ParsedArgs,
148//!     ) -> dynamic_cli::Result<()> {
149//!         let denom = args.get_scalar("denominator")
150//!             .ok_or_else(|| {
151//!                 ExecutionError::CommandFailed(
152//!                     anyhow::anyhow!("Missing Denominator"))})?;
153//!
154//!         let value: f64 = denom.parse()
155//!             .map_err(|_| {
156//!                 ExecutionError::CommandFailed(
157//!                     anyhow::anyhow!("Invalid Denominator"))})?;
158//!
159//!         if value == 0.0 {
160//!             return Err(ExecutionError::CommandFailed(
161//!                     anyhow::anyhow!("Cannot divide by zero")).into());
162//!         }
163//!         Ok(())
164//!     }
165//! }
166//! ```
167//!
168//! ## Stateful Command
169//!
170//! ```
171//! use dynamic_cli::error::ExecutionError;
172//! use dynamic_cli::executor::{CommandHandler, ParsedArgs};
173//! use dynamic_cli::context::ExecutionContext;
174//! use dynamic_cli::Result;
175//!
176//! #[derive(Default)]
177//! struct FileContext {
178//!     current_file: Option<String>,
179//! }
180//!
181//! impl ExecutionContext for FileContext {
182//!     fn as_any(&self) -> &dyn std::any::Any { self }
183//!     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
184//! }
185//!
186//! struct OpenCommand;
187//!
188//! impl CommandHandler for OpenCommand {
189//!     fn execute(
190//!         &self,
191//!         context: &mut dyn ExecutionContext,
192//!         args: &ParsedArgs,
193//!     ) -> Result<()> {
194//!         let ctx = dynamic_cli::context::downcast_mut::<FileContext>(context)
195//!             .ok_or_else(|| ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type")))?;
196//!         
197//!         let filename = args.get_scalar("file")
198//!             .ok_or_else(|| { ExecutionError::CommandFailed(anyhow::anyhow!("Missing file argument"))})?;
199//!         
200//!         ctx.current_file = Some(filename.to_string());
201//!         println!("Opened: {}", filename);
202//!         Ok(())
203//!     }
204//! }
205//! ```
206//!
207//! # Advanced Usage
208//!
209//! ## Dynamic Command Registration
210//!
211//! Commands can be registered dynamically at runtime using trait objects:
212//!
213//! ```
214//! use std::collections::HashMap;
215//! use dynamic_cli::executor::CommandHandler;
216//! # use dynamic_cli::context::ExecutionContext;
217//! # use dynamic_cli::Result;
218//!
219//! // Store commands in a registry
220//! struct CommandRegistry {
221//!     handlers: HashMap<String, Box<dyn CommandHandler>>,
222//! }
223//!
224//! impl CommandRegistry {
225//!     fn new() -> Self {
226//!         Self {
227//!             handlers: HashMap::new(),
228//!         }
229//!     }
230//!     
231//!     fn register(&mut self, name: String, handler: Box<dyn CommandHandler>) {
232//!         self.handlers.insert(name, handler);
233//!     }
234//!     
235//!     fn get(&self, name: &str) -> Option<&Box<dyn CommandHandler>> {
236//!         self.handlers.get(name)
237//!     }
238//! }
239//! ```
240//!
241//! ## Error Handling Pattern
242//!
243//! ```
244//! use dynamic_cli::executor::{CommandHandler, ParsedArgs};
245//! use dynamic_cli::context::ExecutionContext;
246//! use dynamic_cli::error::ExecutionError;
247//! use dynamic_cli::Result;
248//!
249//! struct FileCommand;
250//!
251//! impl CommandHandler for FileCommand {
252//!     fn execute(
253//!         &self,
254//!         _context: &mut dyn ExecutionContext,
255//!         args: &ParsedArgs,
256//!     ) -> Result<()> {
257//!         let path = args.get_scalar("path")
258//!             .ok_or_else(|| { ExecutionError::CommandFailed(anyhow::anyhow!("Missing path argument"))})?;
259//!         
260//!         // Wrap application errors in ExecutionError
261//!         std::fs::read_to_string(path)
262//!             .map_err(|e| ExecutionError::CommandFailed(
263//!                 anyhow::anyhow!("Failed to read file: {}", e)
264//!             ))?;
265//!         
266//!         Ok(())
267//!     }
268//! }
269//! ```
270
271// Public submodules
272pub mod traits;
273
274// Public re-exports for convenience
275pub use crate::parser::ParsedArgs;
276pub use traits::{AsyncCommandHandler, CommandHandler};
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use crate::context::ExecutionContext;
282    use crate::error::ExecutionError;
283    use std::any::Any;
284    use std::collections::HashMap;
285
286    // ============================================================================
287    // INTEGRATION TEST FIXTURES
288    // ============================================================================
289
290    /// Test context for integration tests
291    #[derive(Default)]
292    struct IntegrationContext {
293        log: Vec<String>,
294        state: HashMap<String, String>,
295    }
296
297    impl ExecutionContext for IntegrationContext {
298        fn as_any(&self) -> &dyn Any {
299            self
300        }
301
302        fn as_any_mut(&mut self) -> &mut dyn Any {
303            self
304        }
305    }
306
307    /// Command that logs its execution
308    struct LogCommand;
309
310    impl CommandHandler for LogCommand {
311        fn execute(
312            &self,
313            context: &mut dyn ExecutionContext,
314            args: &ParsedArgs,
315        ) -> crate::error::Result<()> {
316            let ctx =
317                crate::context::downcast_mut::<IntegrationContext>(context).ok_or_else(|| {
318                    ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
319                })?;
320
321            let message = args.get_scalar("message").unwrap_or("default message");
322            ctx.log.push(message.to_string());
323            Ok(())
324        }
325    }
326
327    /// Command that sets state
328    struct SetCommand;
329
330    impl CommandHandler for SetCommand {
331        fn execute(
332            &self,
333            context: &mut dyn ExecutionContext,
334            args: &ParsedArgs,
335        ) -> crate::error::Result<()> {
336            let ctx =
337                crate::context::downcast_mut::<IntegrationContext>(context).ok_or_else(|| {
338                    ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
339                })?;
340
341            if let (Some(key), Some(value)) = (args.get_scalar("key"), args.get_scalar("value")) {
342                ctx.state.insert(key.to_string(), value.to_string());
343            }
344            Ok(())
345        }
346    }
347
348    /// Command that reads state
349    struct GetCommand;
350
351    impl CommandHandler for GetCommand {
352        fn execute(
353            &self,
354            context: &mut dyn ExecutionContext,
355            args: &ParsedArgs,
356        ) -> crate::error::Result<()> {
357            let ctx =
358                crate::context::downcast_mut::<IntegrationContext>(context).ok_or_else(|| {
359                    ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
360                })?;
361
362            if let Some(key) = args.get_scalar("key") {
363                if let Some(value) = ctx.state.get(key) {
364                    ctx.log.push(format!("{} = {}", key, value));
365                } else {
366                    ctx.log.push(format!("{} not found", key));
367                }
368            }
369            Ok(())
370        }
371    }
372
373    // ============================================================================
374    // INTEGRATION TESTS
375    // ============================================================================
376
377    #[test]
378    fn test_module_reexports() {
379        // Verify that CommandHandler is accessible from module root
380        fn _accepts_handler(_: &dyn CommandHandler) {}
381
382        let handler = LogCommand;
383        _accepts_handler(&handler);
384    }
385
386    #[test]
387    fn test_command_sequence() {
388        // Test executing multiple commands in sequence
389        let mut context = IntegrationContext::default();
390
391        // Execute first command
392        let log_cmd = LogCommand;
393        let mut args1 = HashMap::new();
394        args1.insert("message".to_string(), "First".to_string());
395        let args1 = ParsedArgs::from_scalars(args1);
396        log_cmd.execute(&mut context, &args1).unwrap();
397
398        // Execute second command
399        let mut args2 = HashMap::new();
400        args2.insert("message".to_string(), "Second".to_string());
401        let args2 = ParsedArgs::from_scalars(args2);
402        log_cmd.execute(&mut context, &args2).unwrap();
403
404        // Verify both commands executed
405        assert_eq!(context.log.len(), 2);
406        assert_eq!(context.log[0], "First");
407        assert_eq!(context.log[1], "Second");
408    }
409
410    #[test]
411    fn test_stateful_workflow() {
412        // Test a complete workflow with state management
413        let mut context = IntegrationContext::default();
414
415        // Set some values
416        let set_cmd = SetCommand;
417        let mut args1 = HashMap::new();
418        args1.insert("key".to_string(), "name".to_string());
419        args1.insert("value".to_string(), "Alice".to_string());
420        let args1 = ParsedArgs::from_scalars(args1);
421        set_cmd.execute(&mut context, &args1).unwrap();
422
423        let mut args2 = HashMap::new();
424        args2.insert("key".to_string(), "age".to_string());
425        args2.insert("value".to_string(), "30".to_string());
426        let args2 = ParsedArgs::from_scalars(args2);
427        set_cmd.execute(&mut context, &args2).unwrap();
428
429        // Retrieve values
430        let get_cmd = GetCommand;
431        let mut args3 = HashMap::new();
432        args3.insert("key".to_string(), "name".to_string());
433        let args3 = ParsedArgs::from_scalars(args3);
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        let args4 = ParsedArgs::from_scalars(args4);
439        get_cmd.execute(&mut context, &args4).unwrap();
440
441        // Verify workflow
442        assert_eq!(context.state.len(), 2);
443        assert_eq!(context.state.get("name"), Some(&"Alice".to_string()));
444        assert_eq!(context.state.get("age"), Some(&"30".to_string()));
445        assert_eq!(context.log.len(), 2);
446        assert_eq!(context.log[0], "name = Alice");
447        assert_eq!(context.log[1], "age = 30");
448    }
449
450    #[test]
451    fn test_heterogeneous_handler_collection() {
452        // Test storing different handler types in a collection
453        let handlers: Vec<Box<dyn CommandHandler>> = vec![
454            Box::new(LogCommand),
455            Box::new(SetCommand),
456            Box::new(GetCommand),
457        ];
458
459        // Verify we can store different handlers
460        assert_eq!(handlers.len(), 3);
461
462        // Execute each handler
463        let mut context = IntegrationContext::default();
464
465        let mut args1 = HashMap::new();
466        args1.insert("message".to_string(), "test".to_string());
467        let args1 = ParsedArgs::from_scalars(args1);
468        handlers[0].execute(&mut context, &args1).unwrap();
469
470        let mut args2 = HashMap::new();
471        args2.insert("key".to_string(), "k".to_string());
472        args2.insert("value".to_string(), "v".to_string());
473        let args2 = ParsedArgs::from_scalars(args2);
474        handlers[1].execute(&mut context, &args2).unwrap();
475
476        let mut args3 = HashMap::new();
477        args3.insert("key".to_string(), "k".to_string());
478        let args3 = ParsedArgs::from_scalars(args3);
479        handlers[2].execute(&mut context, &args3).unwrap();
480
481        assert_eq!(context.log.len(), 2);
482        assert_eq!(context.state.len(), 1);
483    }
484
485    #[test]
486    fn test_context_isolation_between_commands() {
487        // Verify that context is shared correctly
488        let mut context = IntegrationContext::default();
489
490        let set_cmd = SetCommand;
491        let mut args = HashMap::new();
492        args.insert("key".to_string(), "shared".to_string());
493        args.insert("value".to_string(), "value".to_string());
494        let args = ParsedArgs::from_scalars(args);
495        set_cmd.execute(&mut context, &args).unwrap();
496
497        // Another command can see the state
498        let get_cmd = GetCommand;
499        let mut args2 = HashMap::new();
500        args2.insert("key".to_string(), "shared".to_string());
501        let args2 = ParsedArgs::from_scalars(args2);
502        get_cmd.execute(&mut context, &args2).unwrap();
503
504        assert!(context.log[0].contains("shared = value"));
505    }
506
507    #[test]
508    fn test_error_propagation() {
509        // Test that errors propagate correctly through the handler chain
510        struct FailingCommand;
511
512        impl CommandHandler for FailingCommand {
513            fn execute(
514                &self,
515                _context: &mut dyn ExecutionContext,
516                _args: &ParsedArgs,
517            ) -> crate::error::Result<()> {
518                Err(ExecutionError::CommandFailed(anyhow::anyhow!("Intentional failure")).into())
519            }
520        }
521
522        let handler = FailingCommand;
523        let mut context = IntegrationContext::default();
524        let args = HashMap::new();
525        let args = ParsedArgs::from_scalars(args);
526
527        let result = handler.execute(&mut context, &args);
528
529        assert!(result.is_err());
530        let err_msg = format!("{}", result.unwrap_err());
531        assert!(err_msg.contains("Intentional failure"));
532    }
533
534    #[test]
535    fn test_validation_before_execution() {
536        // Test the intended workflow: validate then execute
537        struct ValidatingCommand;
538
539        impl CommandHandler for ValidatingCommand {
540            fn execute(
541                &self,
542                context: &mut dyn ExecutionContext,
543                _args: &ParsedArgs,
544            ) -> crate::error::Result<()> {
545                let ctx = crate::context::downcast_mut::<IntegrationContext>(context).ok_or_else(
546                    || ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type")),
547                )?;
548                ctx.log.push("executed".to_string());
549                Ok(())
550            }
551
552            fn validate(&self, args: &ParsedArgs) -> crate::error::Result<()> {
553                if args.get_scalar("required").is_none() {
554                    return Err(ExecutionError::CommandFailed(anyhow::anyhow!(
555                        "Missing required argument"
556                    ))
557                    .into());
558                }
559                Ok(())
560            }
561        }
562
563        let handler = ValidatingCommand;
564        let mut context = IntegrationContext::default();
565
566        // Test validation failure
567        let args_invalid = HashMap::new();
568        let args_invalid = ParsedArgs::from_scalars(args_invalid);
569        assert!(handler.validate(&args_invalid).is_err());
570        assert!(handler.execute(&mut context, &args_invalid).is_ok()); // Execute would work
571
572        // Test validation success
573        let mut args_valid = HashMap::new();
574        args_valid.insert("required".to_string(), "value".to_string());
575        let args_valid = ParsedArgs::from_scalars(args_valid);
576        assert!(handler.validate(&args_valid).is_ok());
577        assert!(handler.execute(&mut context, &args_valid).is_ok());
578    }
579
580    #[test]
581    fn test_command_handler_documentation_example() {
582        // Test the example from module documentation
583        #[derive(Default)]
584        struct AppContext {
585            counter: i32,
586        }
587
588        impl ExecutionContext for AppContext {
589            fn as_any(&self) -> &dyn Any {
590                self
591            }
592            fn as_any_mut(&mut self) -> &mut dyn Any {
593                self
594            }
595        }
596
597        struct IncrementCommand;
598
599        impl CommandHandler for IncrementCommand {
600            fn execute(
601                &self,
602                context: &mut dyn ExecutionContext,
603                args: &ParsedArgs,
604            ) -> crate::error::Result<()> {
605                let ctx = crate::context::downcast_mut::<AppContext>(context).ok_or_else(|| {
606                    ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
607                })?;
608
609                let amount: i32 = args
610                    .get_scalar("amount")
611                    .and_then(|s| s.parse().ok())
612                    .unwrap_or(1);
613
614                ctx.counter += amount;
615                Ok(())
616            }
617        }
618
619        let handler = IncrementCommand;
620        let mut context = AppContext::default();
621        let mut args = HashMap::new();
622        args.insert("amount".to_string(), "5".to_string());
623        let args = ParsedArgs::from_scalars(args);
624
625        handler.execute(&mut context, &args).unwrap();
626        assert_eq!(context.counter, 5);
627    }
628
629    #[test]
630    fn test_complex_multi_step_workflow() {
631        // Test a more complex workflow simulating real usage
632        let mut context = IntegrationContext::default();
633
634        // Step 1: Initialize some state
635        let set_cmd = SetCommand;
636        let mut args1 = HashMap::new();
637        args1.insert("key".to_string(), "initialized".to_string());
638        args1.insert("value".to_string(), "true".to_string());
639        let args1 = ParsedArgs::from_scalars(args1);
640        set_cmd.execute(&mut context, &args1).unwrap();
641
642        // Step 2: Log the initialization
643        let log_cmd = LogCommand;
644        let mut args2 = HashMap::new();
645        args2.insert("message".to_string(), "System initialized".to_string());
646        let args2 = ParsedArgs::from_scalars(args2);
647        log_cmd.execute(&mut context, &args2).unwrap();
648
649        // Step 3: Set more state
650        let mut args3 = HashMap::new();
651        args3.insert("key".to_string(), "user".to_string());
652        args3.insert("value".to_string(), "admin".to_string());
653        let args3 = ParsedArgs::from_scalars(args3);
654        set_cmd.execute(&mut context, &args3).unwrap();
655
656        // Step 4: Query state
657        let get_cmd = GetCommand;
658        let mut args4 = HashMap::new();
659        args4.insert("key".to_string(), "user".to_string());
660        let args4 = ParsedArgs::from_scalars(args4);
661        get_cmd.execute(&mut context, &args4).unwrap();
662
663        // Verify the complete workflow
664        assert_eq!(context.state.len(), 2);
665        assert_eq!(context.log.len(), 2);
666        assert_eq!(context.log[0], "System initialized");
667        assert_eq!(context.log[1], "user = admin");
668    }
669}