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