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}