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}