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}