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 async_trait::async_trait;
61use std::collections::HashMap;
62
63/// Trait for command implementations
64///
65/// Each command in the CLI/REPL application must implement this trait.
66/// The trait is designed to be object-safe, allowing commands to be
67/// stored and invoked dynamically through trait objects.
68///
69/// # Object Safety
70///
71/// This trait is intentionally object-safe (can be used as `dyn CommandHandler`).
72/// **Do not add methods with generic type parameters**, as this would break
73/// object safety and prevent dynamic dispatch.
74///
75/// # Thread Safety
76///
77/// Implementations must be `Send + Sync` to allow:
78/// - Sharing command handlers across threads
79/// - Safe concurrent access to the command registry
80/// - Future async execution support
81///
82/// # Execution Flow
83///
84/// 1. Parser converts user input to `HashMap<String, String>`
85/// 2. Validator checks argument constraints
86/// 3. `validate()` is called for custom validation (optional)
87/// 4. `execute()` is called with validated arguments
88///
89/// # Example
90///
91/// ```
92/// use std::collections::HashMap;
93/// use dynamic_cli::error::ExecutionError;
94/// use dynamic_cli::executor::CommandHandler;
95/// use dynamic_cli::context::ExecutionContext;
96/// use dynamic_cli::Result;
97///
98/// struct GreetCommand;
99///
100/// impl CommandHandler for GreetCommand {
101/// fn execute(
102/// &self,
103/// _context: &mut dyn ExecutionContext,
104/// args: &HashMap<String, String>,
105/// ) -> Result<()> {
106/// let name = args.get("name")
107/// .ok_or_else(|| {
108/// ExecutionError::CommandFailed(
109/// anyhow::anyhow!("Missing 'name' argument")
110/// )
111/// })?;
112///
113/// let greeting = if let Some(formal) = args.get("formal") {
114/// if formal == "true" {
115/// format!("Good day, {}.", name)
116/// } else {
117/// format!("Hi, {}!", name)
118/// }
119/// } else {
120/// format!("Hello, {}!", name)
121/// };
122///
123/// println!("{}", greeting);
124/// Ok(())
125/// }
126///
127/// fn validate(&self, args: &HashMap<String, String>) -> Result<()> {
128/// // Custom validation: name must not be empty
129/// if let Some(name) = args.get("name") {
130/// if name.trim().is_empty() {
131/// return Err(ExecutionError::CommandFailed(
132/// anyhow::anyhow!("Name cannot be empty")
133/// ).into());
134/// }
135/// }
136/// Ok(())
137/// }
138/// }
139/// ```
140pub trait CommandHandler: Send + Sync {
141 /// Execute the command with the given context and arguments
142 ///
143 /// This is the main entry point for command execution. It receives:
144 /// - A mutable reference to the execution context (for shared state)
145 /// - A map of argument names to their string values
146 ///
147 /// # Arguments
148 ///
149 /// * `context` - Mutable execution context for sharing state between commands.
150 /// Use `downcast_ref` or `downcast_mut` from the `context` module
151 /// to access your specific context type.
152 ///
153 /// * `args` - Parsed and validated arguments as name-value pairs.
154 /// All values are strings; type conversion should be done
155 /// within the handler if needed.
156 ///
157 /// # Returns
158 ///
159 /// - `Ok(())` if execution succeeds
160 /// - `Err(DynamicCliError)` if execution fails
161 ///
162 /// # Errors
163 ///
164 /// Implementations should return errors for:
165 /// - Invalid argument values (caught by validate, but can be rechecked)
166 /// - Execution failures (I/O errors, computation errors, etc.)
167 /// - Invalid context state
168 ///
169 /// Use `ExecutionError::CommandFailed` to wrap application-specific errors:
170 /// ```ignore
171 /// Err(ExecutionError::CommandFailed(anyhow::anyhow!("Details")).into())
172 /// ```
173 ///
174 /// # Example
175 ///
176 /// ```
177 /// # use std::collections::HashMap;
178 /// # use dynamic_cli::error::ExecutionError;
179 /// # use dynamic_cli::executor::CommandHandler;
180 /// # use dynamic_cli::context::ExecutionContext;
181 /// # use dynamic_cli::Result;
182 /// #
183 /// struct FileCommand;
184 ///
185 /// impl CommandHandler for FileCommand {
186 /// fn execute(
187 /// &self,
188 /// _context: &mut dyn ExecutionContext,
189 /// args: &HashMap<String, String>,
190 /// ) -> Result<()> {
191 /// let path = args.get("path")
192 /// .ok_or_else(|| {
193 /// ExecutionError::CommandFailed(
194 /// anyhow::anyhow!("Missing path argument")
195 /// )
196 /// })?;
197 ///
198 /// // Perform the actual work
199 /// let content = std::fs::read_to_string(path)
200 /// .map_err(|e| {
201 /// ExecutionError::CommandFailed(anyhow::anyhow!("Failed to read file: {}", e))
202 /// })?;
203 ///
204 /// println!("File contains {} bytes", content.len());
205 /// Ok(())
206 /// }
207 /// }
208 /// ```
209 fn execute(
210 &self,
211 context: &mut dyn ExecutionContext,
212 args: &HashMap<String, String>,
213 ) -> Result<()>;
214
215 /// Optional custom validation for arguments
216 ///
217 /// This method is called after the standard validation (type checking,
218 /// required arguments, etc.) but before execution. It allows commands
219 /// to implement custom validation logic.
220 ///
221 /// # Default Implementation
222 ///
223 /// The default implementation accepts all arguments (returns `Ok(())`).
224 /// Override this method only if you need custom validation.
225 ///
226 /// # Arguments
227 ///
228 /// * `args` - The arguments to validate
229 ///
230 /// # Returns
231 ///
232 /// - `Ok(())` if validation succeeds
233 /// - `Err(DynamicCliError)` if validation fails
234 ///
235 /// # Example
236 ///
237 /// ```
238 /// # use std::collections::HashMap;
239 /// # use dynamic_cli::executor::CommandHandler;
240 /// # use dynamic_cli::context::ExecutionContext;
241 /// # use dynamic_cli::error::ExecutionError;
242 /// # use dynamic_cli::Result;
243 /// #
244 /// struct RangeCommand;
245 ///
246 /// impl CommandHandler for RangeCommand {
247 /// fn execute(
248 /// &self,
249 /// _context: &mut dyn ExecutionContext,
250 /// args: &HashMap<String, String>,
251 /// ) -> Result<()> {
252 /// // Execution logic here
253 /// Ok(())
254 /// }
255 ///
256 /// fn validate(&self, args: &HashMap<String, String>) -> Result<()> {
257 /// // Custom validation: ensure min < max
258 /// if let (Some(min), Some(max)) = (args.get("min"), args.get("max")) {
259 /// let min_val: f64 = min.parse()
260 /// .map_err(|_| {
261 /// ExecutionError::CommandFailed(anyhow::anyhow!("Invalid min value"))
262 /// })?;
263 /// let max_val: f64 = max.parse()
264 /// .map_err(|_| {ExecutionError::CommandFailed(anyhow::anyhow!("Invalid max value"))})?;
265 ///
266 /// if min_val >= max_val {
267 /// return Err(ExecutionError::CommandFailed(anyhow::anyhow!("min must be less than max")).into());
268 /// }
269 /// }
270 /// Ok(())
271 /// }
272 /// }
273 /// ```
274 fn validate(&self, _args: &HashMap<String, String>) -> Result<()> {
275 Ok(())
276 }
277}
278
279/// Async counterpart of [`CommandHandler`].
280///
281/// Additive to `CommandHandler` (see DD-022) β it does not replace it.
282/// Implementations use this trait when their command body needs to perform
283/// async I/O (network calls, streaming, etc.). The signatures deliberately
284/// mirror `CommandHandler` exactly, `execute`/`validate` aside from the
285/// `async` keyword, so that migrating a handler from sync to async is a
286/// mechanical change.
287///
288/// # Object Safety
289///
290/// Made `dyn`-compatible via `#[async_trait]` (which desugars `async fn` to
291/// a boxed, pinned future under the hood). Stored as `Box<dyn
292/// AsyncCommandHandler>` in the registry, exactly like `CommandHandler` is
293/// stored as `Box<dyn CommandHandler>`.
294///
295/// # Thread Safety
296///
297/// Same constraint as `CommandHandler`: `Send + Sync` is required so the
298/// handler can be shared across the registry and, transitively, across
299/// threads if the application needs it.
300///
301/// # Why a separate trait instead of an async `CommandHandler`?
302///
303/// Existing sync `CommandHandler` implementations (including downstream
304/// consumers) must keep compiling unchanged. See DD-022 for the full
305/// rationale, including why `tokio` is not a dependency of `dynamic-cli`
306/// itself and why driving the returned future via
307/// `futures::executor::block_on` at the dispatch site is safe.
308///
309/// # Example
310///
311/// ```
312/// use std::collections::HashMap;
313/// use async_trait::async_trait;
314/// use dynamic_cli::executor::AsyncCommandHandler;
315/// use dynamic_cli::context::ExecutionContext;
316/// use dynamic_cli::error::ExecutionError;
317/// use dynamic_cli::Result;
318///
319/// struct FetchCommand;
320///
321/// #[async_trait]
322/// impl AsyncCommandHandler for FetchCommand {
323/// async fn execute(
324/// &self,
325/// _context: &mut dyn ExecutionContext,
326/// args: &HashMap<String, String>,
327/// ) -> Result<()> {
328/// let url = args.get("url").ok_or_else(|| {
329/// ExecutionError::CommandFailed(anyhow::anyhow!("Missing 'url' argument"))
330/// })?;
331/// // Real implementations would `.await` an async HTTP call here.
332/// println!("Fetching {url}...");
333/// Ok(())
334/// }
335///
336/// async fn validate(&self, args: &HashMap<String, String>) -> Result<()> {
337/// if !args.contains_key("url") {
338/// return Err(ExecutionError::CommandFailed(anyhow::anyhow!("url is required")).into());
339/// }
340/// Ok(())
341/// }
342/// }
343/// ```
344#[async_trait]
345pub trait AsyncCommandHandler: Send + Sync {
346 /// Async equivalent of [`CommandHandler::execute`]. Same contract:
347 /// receives the mutable execution context and the parsed arguments,
348 /// returns `Ok(())` on success or a `DynamicCliError` on failure.
349 async fn execute(
350 &self,
351 context: &mut dyn ExecutionContext,
352 args: &HashMap<String, String>,
353 ) -> Result<()>;
354
355 /// Async equivalent of [`CommandHandler::validate`]. Same contract and
356 /// same default (accepts all arguments) β override only for custom
357 /// validation logic.
358 async fn validate(&self, _args: &HashMap<String, String>) -> Result<()> {
359 Ok(())
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::error::ExecutionError;
367 use std::any::Any;
368 use std::sync::{Arc, Mutex};
369
370 // ============================================================================
371 // TEST FIXTURES
372 // ============================================================================
373
374 /// Simple test context for unit tests
375 #[derive(Default)]
376 struct TestContext {
377 state: String,
378 }
379
380 impl ExecutionContext for TestContext {
381 fn as_any(&self) -> &dyn Any {
382 self
383 }
384
385 fn as_any_mut(&mut self) -> &mut dyn Any {
386 self
387 }
388 }
389
390 /// Simple command that prints to context
391 struct HelloCommand;
392
393 impl CommandHandler for HelloCommand {
394 fn execute(
395 &self,
396 context: &mut dyn ExecutionContext,
397 args: &HashMap<String, String>,
398 ) -> Result<()> {
399 let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
400 ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
401 })?;
402
403 let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
404 ctx.state = format!("Hello, {}!", name);
405 Ok(())
406 }
407 }
408
409 /// Command with custom validation
410 struct ValidatedCommand;
411
412 impl CommandHandler for ValidatedCommand {
413 fn execute(
414 &self,
415 _context: &mut dyn ExecutionContext,
416 _args: &HashMap<String, String>,
417 ) -> Result<()> {
418 Ok(())
419 }
420
421 fn validate(&self, args: &HashMap<String, String>) -> Result<()> {
422 // Require "count" argument to be present and > 0
423 if let Some(count) = args.get("count") {
424 let count_val: i32 = count.parse().map_err(|_| {
425 ExecutionError::CommandFailed(anyhow::anyhow!("count must be an integer"))
426 })?;
427
428 if count_val <= 0 {
429 return Err(ExecutionError::CommandFailed(anyhow::anyhow!(
430 "count must be positive"
431 ))
432 .into());
433 }
434 } else {
435 return Err(
436 ExecutionError::CommandFailed(anyhow::anyhow!("count is required")).into(),
437 );
438 }
439 Ok(())
440 }
441 }
442
443 /// Command that fails during execution
444 struct FailingCommand;
445
446 impl CommandHandler for FailingCommand {
447 fn execute(
448 &self,
449 _context: &mut dyn ExecutionContext,
450 _args: &HashMap<String, String>,
451 ) -> Result<()> {
452 Err(ExecutionError::CommandFailed(anyhow::anyhow!("Simulated failure")).into())
453 }
454 }
455
456 /// Command that modifies context
457 struct StatefulCommand;
458
459 impl CommandHandler for StatefulCommand {
460 fn execute(
461 &self,
462 context: &mut dyn ExecutionContext,
463 args: &HashMap<String, String>,
464 ) -> Result<()> {
465 let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
466 ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
467 })?;
468
469 let value = args.get("value").map(|s| s.as_str()).unwrap_or("default");
470 ctx.state.push_str(value);
471 Ok(())
472 }
473 }
474
475 // ============================================================================
476 // BASIC FUNCTIONALITY TESTS
477 // ============================================================================
478
479 #[test]
480 fn test_basic_execution() {
481 let handler = HelloCommand;
482 let mut context = TestContext::default();
483 let mut args = HashMap::new();
484 args.insert("name".to_string(), "Rust".to_string());
485
486 let result = handler.execute(&mut context, &args);
487
488 assert!(result.is_ok());
489 assert_eq!(context.state, "Hello, Rust!");
490 }
491
492 #[test]
493 fn test_execution_without_args() {
494 let handler = HelloCommand;
495 let mut context = TestContext::default();
496 let args = HashMap::new();
497
498 let result = handler.execute(&mut context, &args);
499
500 assert!(result.is_ok());
501 assert_eq!(context.state, "Hello, World!");
502 }
503
504 #[test]
505 fn test_execution_with_empty_name() {
506 let handler = HelloCommand;
507 let mut context = TestContext::default();
508 let mut args = HashMap::new();
509 args.insert("name".to_string(), "".to_string());
510
511 let result = handler.execute(&mut context, &args);
512
513 assert!(result.is_ok());
514 assert_eq!(context.state, "Hello, !");
515 }
516
517 // ============================================================================
518 // VALIDATION TESTS
519 // ============================================================================
520
521 #[test]
522 fn test_default_validation_accepts_all() {
523 let handler = HelloCommand;
524 let mut args = HashMap::new();
525 args.insert("random".to_string(), "value".to_string());
526
527 let result = handler.validate(&args);
528
529 assert!(result.is_ok());
530 }
531
532 #[test]
533 fn test_custom_validation_success() {
534 let handler = ValidatedCommand;
535 let mut args = HashMap::new();
536 args.insert("count".to_string(), "5".to_string());
537
538 let result = handler.validate(&args);
539
540 assert!(result.is_ok());
541 }
542
543 #[test]
544 fn test_custom_validation_missing_arg() {
545 let handler = ValidatedCommand;
546 let args = HashMap::new();
547
548 let result = handler.validate(&args);
549
550 assert!(result.is_err());
551 let err_msg = format!("{}", result.unwrap_err());
552 assert!(err_msg.contains("required"));
553 }
554
555 #[test]
556 fn test_custom_validation_invalid_value() {
557 let handler = ValidatedCommand;
558 let mut args = HashMap::new();
559 args.insert("count".to_string(), "0".to_string());
560
561 let result = handler.validate(&args);
562
563 assert!(result.is_err());
564 let err_msg = format!("{}", result.unwrap_err());
565 assert!(err_msg.contains("positive"));
566 }
567
568 #[test]
569 fn test_custom_validation_non_integer() {
570 let handler = ValidatedCommand;
571 let mut args = HashMap::new();
572 args.insert("count".to_string(), "abc".to_string());
573
574 let result = handler.validate(&args);
575
576 assert!(result.is_err());
577 let err_msg = format!("{}", result.unwrap_err());
578 assert!(err_msg.contains("integer"));
579 }
580
581 // ============================================================================
582 // ERROR HANDLING TESTS
583 // ============================================================================
584
585 #[test]
586 fn test_execution_failure() {
587 let handler = FailingCommand;
588 let mut context = TestContext::default();
589 let args = HashMap::new();
590
591 let result = handler.execute(&mut context, &args);
592
593 assert!(result.is_err());
594 let err_msg = format!("{}", result.unwrap_err());
595 assert!(err_msg.contains("Simulated failure"));
596 }
597
598 #[test]
599 fn test_context_downcast_failure() {
600 // Use a different context type to trigger downcast failure
601 #[derive(Default)]
602 struct WrongContext;
603
604 impl ExecutionContext for WrongContext {
605 fn as_any(&self) -> &dyn Any {
606 self
607 }
608
609 fn as_any_mut(&mut self) -> &mut dyn Any {
610 self
611 }
612 }
613
614 let handler = HelloCommand;
615 let mut wrong_context = WrongContext::default();
616 let args = HashMap::new();
617
618 let result = handler.execute(&mut wrong_context, &args);
619
620 assert!(result.is_err());
621 let err_msg = format!("{}", result.unwrap_err());
622 assert!(err_msg.contains("Wrong context type"));
623 }
624
625 // ============================================================================
626 // STATE MODIFICATION TESTS
627 // ============================================================================
628
629 #[test]
630 fn test_context_state_modification() {
631 let handler = StatefulCommand;
632 let mut context = TestContext::default();
633 context.state = "initial".to_string();
634 let mut args = HashMap::new();
635 args.insert("value".to_string(), "_modified".to_string());
636
637 let result = handler.execute(&mut context, &args);
638
639 assert!(result.is_ok());
640 assert_eq!(context.state, "initial_modified");
641 }
642
643 #[test]
644 fn test_multiple_executions_preserve_state() {
645 let handler = StatefulCommand;
646 let mut context = TestContext::default();
647
648 // First execution
649 let mut args1 = HashMap::new();
650 args1.insert("value".to_string(), "first".to_string());
651 handler.execute(&mut context, &args1).unwrap();
652 assert_eq!(context.state, "first");
653
654 // Second execution
655 let mut args2 = HashMap::new();
656 args2.insert("value".to_string(), "_second".to_string());
657 handler.execute(&mut context, &args2).unwrap();
658 assert_eq!(context.state, "first_second");
659 }
660
661 // ============================================================================
662 // TRAIT OBJECT TESTS
663 // ============================================================================
664
665 #[test]
666 fn test_trait_object_usage() {
667 // Verify that CommandHandler can be used as a trait object
668 let handler: Box<dyn CommandHandler> = Box::new(HelloCommand);
669 let mut context = TestContext::default();
670 let mut args = HashMap::new();
671 args.insert("name".to_string(), "TraitObject".to_string());
672
673 let result = handler.execute(&mut context, &args);
674
675 assert!(result.is_ok());
676 assert_eq!(context.state, "Hello, TraitObject!");
677 }
678
679 #[test]
680 fn test_multiple_trait_objects() {
681 // Store multiple handlers as trait objects
682 let handlers: Vec<Box<dyn CommandHandler>> =
683 vec![Box::new(HelloCommand), Box::new(StatefulCommand)];
684
685 let mut context = TestContext::default();
686
687 // Execute first handler
688 let mut args1 = HashMap::new();
689 args1.insert("name".to_string(), "First".to_string());
690 handlers[0].execute(&mut context, &args1).unwrap();
691 assert_eq!(context.state, "Hello, First!");
692
693 // Execute second handler
694 context.state.clear();
695 let mut args2 = HashMap::new();
696 args2.insert("value".to_string(), "Second".to_string());
697 handlers[1].execute(&mut context, &args2).unwrap();
698 assert_eq!(context.state, "Second");
699 }
700
701 // ============================================================================
702 // THREAD SAFETY TESTS
703 // ============================================================================
704
705 #[test]
706 fn test_send_sync_requirement() {
707 // This test verifies that CommandHandler is Send + Sync
708 // by using it in a multi-threaded context
709 let handler: Arc<dyn CommandHandler> = Arc::new(HelloCommand);
710
711 // Clone the Arc to simulate sharing across threads
712 let handler_clone = handler.clone();
713
714 // This compilation test ensures Send + Sync are satisfied
715 let _ = std::thread::spawn(move || {
716 let _h = handler_clone;
717 });
718 }
719
720 #[test]
721 fn test_concurrent_validation() {
722 // Test that validation can be called from multiple threads
723 let handler = Arc::new(ValidatedCommand);
724 let handler_clone = handler.clone();
725
726 let handle = std::thread::spawn(move || {
727 let mut args = HashMap::new();
728 args.insert("count".to_string(), "10".to_string());
729 handler_clone.validate(&args)
730 });
731
732 let mut args = HashMap::new();
733 args.insert("count".to_string(), "5".to_string());
734 let result1 = handler.validate(&args);
735
736 let result2 = handle.join().unwrap();
737
738 assert!(result1.is_ok());
739 assert!(result2.is_ok());
740 }
741
742 // ============================================================================
743 // EDGE CASES
744 // ============================================================================
745
746 #[test]
747 fn test_empty_args() {
748 let handler = StatefulCommand;
749 let mut context = TestContext::default();
750 let args = HashMap::new();
751
752 // Should use default value
753 let result = handler.execute(&mut context, &args);
754
755 assert!(result.is_ok());
756 assert_eq!(context.state, "default");
757 }
758
759 #[test]
760 fn test_args_with_special_characters() {
761 let handler = HelloCommand;
762 let mut context = TestContext::default();
763 let mut args = HashMap::new();
764 args.insert("name".to_string(), "Hello, δΈη! π".to_string());
765
766 let result = handler.execute(&mut context, &args);
767
768 assert!(result.is_ok());
769 assert_eq!(context.state, "Hello, Hello, δΈη! π!");
770 }
771
772 #[test]
773 fn test_very_long_argument() {
774 let handler = HelloCommand;
775 let mut context = TestContext::default();
776 let mut args = HashMap::new();
777 let long_name = "x".repeat(10000);
778 args.insert("name".to_string(), long_name.clone());
779
780 let result = handler.execute(&mut context, &args);
781
782 assert!(result.is_ok());
783 assert!(context.state.contains(&long_name));
784 }
785
786 // ============================================================================
787 // SHARED STATE TESTS
788 // ============================================================================
789
790 #[test]
791 fn test_shared_mutable_context() {
792 // Test that context can be safely modified by multiple commands
793 let handler1 = StatefulCommand;
794 let handler2 = StatefulCommand;
795 let mut context = TestContext::default();
796
797 let mut args1 = HashMap::new();
798 args1.insert("value".to_string(), "A".to_string());
799 handler1.execute(&mut context, &args1).unwrap();
800
801 let mut args2 = HashMap::new();
802 args2.insert("value".to_string(), "B".to_string());
803 handler2.execute(&mut context, &args2).unwrap();
804
805 assert_eq!(context.state, "AB");
806 }
807
808 // Test to ensure the trait is indeed object-safe at compile time
809 #[test]
810 fn test_object_safety_compile_time() {
811 // This function signature requires CommandHandler to be object-safe
812 fn _accepts_trait_object(_: &dyn CommandHandler) {}
813
814 // If this compiles, the trait is object-safe
815 let handler = HelloCommand;
816 _accepts_trait_object(&handler);
817 }
818
819 // Test that demonstrates why we can't have generic methods
820 // (This is a documentation test, not an actual test that runs)
821 /// ```compile_fail
822 /// use dynamic_cli::executor::CommandHandler;
823 ///
824 /// trait BrokenHandler: CommandHandler {
825 /// fn generic_method<T>(&self, value: T);
826 /// }
827 ///
828 /// // This would fail because trait objects can't have generic methods
829 /// fn use_as_trait_object(handler: &dyn BrokenHandler) {
830 /// // Cannot call generic_method on trait object
831 /// }
832 /// ```
833 #[allow(dead_code)]
834 fn test_no_generic_methods_documentation() {}
835
836 // ============================================================================
837 // AsyncCommandHandler TESTS (DD-022)
838 // ============================================================================
839
840 /// Async command that writes to the test context, mirroring `HelloCommand`.
841 struct AsyncHelloCommand;
842
843 #[async_trait]
844 impl AsyncCommandHandler for AsyncHelloCommand {
845 async fn execute(
846 &self,
847 context: &mut dyn ExecutionContext,
848 args: &HashMap<String, String>,
849 ) -> Result<()> {
850 let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
851 ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
852 })?;
853 let name = args.get("name").map(|s| s.as_str()).unwrap_or("World");
854 ctx.state = format!("Hello, {}!", name);
855 Ok(())
856 }
857 }
858
859 /// Async command with custom validation, mirroring `ValidatedCommand`.
860 struct AsyncValidatedCommand;
861
862 #[async_trait]
863 impl AsyncCommandHandler for AsyncValidatedCommand {
864 async fn execute(
865 &self,
866 _context: &mut dyn ExecutionContext,
867 _args: &HashMap<String, String>,
868 ) -> Result<()> {
869 Ok(())
870 }
871
872 async fn validate(&self, args: &HashMap<String, String>) -> Result<()> {
873 if !args.contains_key("count") {
874 return Err(
875 ExecutionError::CommandFailed(anyhow::anyhow!("count is required")).into(),
876 );
877 }
878 Ok(())
879 }
880 }
881
882 /// Async command that fails during execution, mirroring `FailingCommand`.
883 struct AsyncFailingCommand;
884
885 #[async_trait]
886 impl AsyncCommandHandler for AsyncFailingCommand {
887 async fn execute(
888 &self,
889 _context: &mut dyn ExecutionContext,
890 _args: &HashMap<String, String>,
891 ) -> Result<()> {
892 Err(ExecutionError::CommandFailed(anyhow::anyhow!("Simulated async failure")).into())
893 }
894 }
895
896 #[test]
897 fn test_async_basic_execution() {
898 let handler = AsyncHelloCommand;
899 let mut context = TestContext::default();
900 let mut args = HashMap::new();
901 args.insert("name".to_string(), "Rust".to_string());
902
903 let result = futures::executor::block_on(handler.execute(&mut context, &args));
904
905 assert!(result.is_ok());
906 assert_eq!(context.state, "Hello, Rust!");
907 }
908
909 #[test]
910 fn test_async_default_validation_accepts_all() {
911 let handler = AsyncHelloCommand;
912 let mut args = HashMap::new();
913 args.insert("random".to_string(), "value".to_string());
914
915 let result = futures::executor::block_on(handler.validate(&args));
916
917 assert!(result.is_ok());
918 }
919
920 #[test]
921 fn test_async_custom_validation_missing_arg() {
922 let handler = AsyncValidatedCommand;
923 let args = HashMap::new();
924
925 let result = futures::executor::block_on(handler.validate(&args));
926
927 assert!(result.is_err());
928 let err_msg = format!("{}", result.unwrap_err());
929 assert!(err_msg.contains("required"));
930 }
931
932 #[test]
933 fn test_async_custom_validation_success() {
934 let handler = AsyncValidatedCommand;
935 let mut args = HashMap::new();
936 args.insert("count".to_string(), "5".to_string());
937
938 let result = futures::executor::block_on(handler.validate(&args));
939
940 assert!(result.is_ok());
941 }
942
943 #[test]
944 fn test_async_execution_failure() {
945 let handler = AsyncFailingCommand;
946 let mut context = TestContext::default();
947 let args = HashMap::new();
948
949 let result = futures::executor::block_on(handler.execute(&mut context, &args));
950
951 assert!(result.is_err());
952 let err_msg = format!("{}", result.unwrap_err());
953 assert!(err_msg.contains("Simulated async failure"));
954 }
955
956 #[test]
957 fn test_async_trait_object_usage() {
958 // Verify that AsyncCommandHandler can be used as a trait object β
959 // the core object-safety guarantee DD-022 depends on.
960 let handler: Box<dyn AsyncCommandHandler> = Box::new(AsyncHelloCommand);
961 let mut context = TestContext::default();
962 let mut args = HashMap::new();
963 args.insert("name".to_string(), "TraitObject".to_string());
964
965 let result = futures::executor::block_on(handler.execute(&mut context, &args));
966
967 assert!(result.is_ok());
968 assert_eq!(context.state, "Hello, TraitObject!");
969 }
970
971 #[test]
972 fn test_async_send_sync_requirement() {
973 // Verifies AsyncCommandHandler is Send + Sync by sharing it across
974 // threads via Arc β same pattern as test_send_sync_requirement above.
975 let handler: Arc<dyn AsyncCommandHandler> = Arc::new(AsyncHelloCommand);
976 let handler_clone = handler.clone();
977
978 let _ = std::thread::spawn(move || {
979 let _h = handler_clone;
980 });
981 }
982
983 #[test]
984 fn test_async_object_safety_compile_time() {
985 // If this compiles, AsyncCommandHandler is dyn-compatible.
986 fn _accepts_trait_object(_: &dyn AsyncCommandHandler) {}
987 _accepts_trait_object(&AsyncHelloCommand);
988 }
989}