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
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/// 0. `expected_fault_tolerance()` is checked once, at registration time
84/// — before any of the steps below, and before this handler ever sees
85/// a command line.
86/// 1. Parser converts user input to [`crate::parser::ParsedArgs`]
87/// 2. Validator checks argument constraints
88/// 3. `validate()` is called for custom validation (optional)
89/// 4. `execute()` is called with validated arguments
90///
91/// # Example
92///
93/// ```
94/// use dynamic_cli::error::ExecutionError;
95/// use dynamic_cli::executor::{CommandHandler, ParsedArgs};
96/// use dynamic_cli::context::ExecutionContext;
97/// use dynamic_cli::Result;
98///
99/// struct GreetCommand;
100///
101/// impl CommandHandler for GreetCommand {
102/// fn execute(
103/// &self,
104/// _context: &mut dyn ExecutionContext,
105/// args: &ParsedArgs,
106/// ) -> Result<()> {
107/// let name = args.get_scalar("name")
108/// .ok_or_else(|| {
109/// ExecutionError::CommandFailed(
110/// anyhow::anyhow!("Missing 'name' argument")
111/// )
112/// })?;
113///
114/// let greeting = if let Some(formal) = args.get_scalar("formal") {
115/// if formal == "true" {
116/// format!("Good day, {}.", name)
117/// } else {
118/// format!("Hi, {}!", name)
119/// }
120/// } else {
121/// format!("Hello, {}!", name)
122/// };
123///
124/// println!("{}", greeting);
125/// Ok(())
126/// }
127///
128/// fn validate(&self, args: &ParsedArgs) -> Result<()> {
129/// // Custom validation: name must not be empty
130/// if let Some(name) = args.get_scalar("name") {
131/// if name.trim().is_empty() {
132/// return Err(ExecutionError::CommandFailed(
133/// anyhow::anyhow!("Name cannot be empty")
134/// ).into());
135/// }
136/// }
137/// Ok(())
138/// }
139/// }
140/// ```
141pub trait CommandHandler: Send + Sync {
142 /// Execute the command with the given context and arguments
143 ///
144 /// This is the main entry point for command execution. It receives:
145 /// - A mutable reference to the execution context (for shared state)
146 /// - A map of argument names to their string values
147 ///
148 /// # Arguments
149 ///
150 /// * `context` - Mutable execution context for sharing state between commands.
151 /// Use `downcast_ref` or `downcast_mut` from the `context` module
152 /// to access your specific context type.
153 ///
154 /// * `args` - Parsed and validated arguments as name-value pairs.
155 /// All values are strings; type conversion should be done
156 /// within the handler if needed.
157 ///
158 /// # Returns
159 ///
160 /// - `Ok(())` if execution succeeds
161 /// - `Err(DynamicCliError)` if execution fails
162 ///
163 /// # Errors
164 ///
165 /// Implementations should return errors for:
166 /// - Invalid argument values (caught by validate, but can be rechecked)
167 /// - Execution failures (I/O errors, computation errors, etc.)
168 /// - Invalid context state
169 ///
170 /// Use `ExecutionError::CommandFailed` to wrap application-specific errors:
171 /// ```ignore
172 /// Err(ExecutionError::CommandFailed(anyhow::anyhow!("Details")).into())
173 /// ```
174 ///
175 /// # Example
176 ///
177 /// ```
178 /// # use dynamic_cli::error::ExecutionError;
179 /// # use dynamic_cli::executor::{CommandHandler, ParsedArgs};
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: &ParsedArgs,
190 /// ) -> Result<()> {
191 /// let path = args.get_scalar("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(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()>;
210
211 /// Optional custom validation for arguments
212 ///
213 /// This method is called after the standard validation (type checking,
214 /// required arguments, etc.) but before execution. It allows commands
215 /// to implement custom validation logic.
216 ///
217 /// # Default Implementation
218 ///
219 /// The default implementation accepts all arguments (returns `Ok(())`).
220 /// Override this method only if you need custom validation.
221 ///
222 /// # Arguments
223 ///
224 /// * `args` - The arguments to validate
225 ///
226 /// # Returns
227 ///
228 /// - `Ok(())` if validation succeeds
229 /// - `Err(DynamicCliError)` if validation fails
230 ///
231 /// # Example
232 ///
233 /// ```
234 /// # use dynamic_cli::executor::{CommandHandler, ParsedArgs};
235 /// # use dynamic_cli::context::ExecutionContext;
236 /// # use dynamic_cli::error::ExecutionError;
237 /// # use dynamic_cli::Result;
238 /// #
239 /// struct RangeCommand;
240 ///
241 /// impl CommandHandler for RangeCommand {
242 /// fn execute(
243 /// &self,
244 /// _context: &mut dyn ExecutionContext,
245 /// args: &ParsedArgs,
246 /// ) -> Result<()> {
247 /// // Execution logic here
248 /// Ok(())
249 /// }
250 ///
251 /// fn validate(&self, args: &ParsedArgs) -> Result<()> {
252 /// // Custom validation: ensure min < max
253 /// if let (Some(min), Some(max)) = (args.get_scalar("min"), args.get_scalar("max")) {
254 /// let min_val: f64 = min.parse()
255 /// .map_err(|_| {
256 /// ExecutionError::CommandFailed(anyhow::anyhow!("Invalid min value"))
257 /// })?;
258 /// let max_val: f64 = max.parse()
259 /// .map_err(|_| {ExecutionError::CommandFailed(anyhow::anyhow!("Invalid max value"))})?;
260 ///
261 /// if min_val >= max_val {
262 /// return Err(ExecutionError::CommandFailed(anyhow::anyhow!("min must be less than max")).into());
263 /// }
264 /// }
265 /// Ok(())
266 /// }
267 /// }
268 /// ```
269 fn validate(&self, _args: &ParsedArgs) -> Result<()> {
270 Ok(())
271 }
272
273 /// Declare an expectation about this command's configured
274 /// `continue_on_failure`, if the handler's author has one.
275 ///
276 /// Purely informational at the trait level: `CommandRegistry::register_sync`
277 /// compares this value (when `Some`) against the `continue_on_failure`
278 /// the YAML config actually declares for this command, and rejects
279 /// registration on a contradiction (`RegistryError::FaultToleranceMismatch`)
280 /// — catching a misconfiguration at startup instead of at first
281 /// use. It does not, and cannot, verify that the handler's own code
282 /// actually behaves as declared; that remains the author's
283 /// responsibility, covered by the application's own tests.
284 ///
285 /// # Default Implementation
286 ///
287 /// `None` — no opinion, no check performed. Override only if this
288 /// handler's `execute()` was written under a specific assumption about
289 /// whether the chain may safely continue past its failure.
290 ///
291 /// # Example
292 ///
293 /// ```
294 /// # use dynamic_cli::executor::{CommandHandler, ParsedArgs};
295 /// # use dynamic_cli::context::ExecutionContext;
296 /// # use dynamic_cli::Result;
297 /// #
298 /// /// A command whose failure must always stop the chain — the handler
299 /// /// author never wants this one silently skipped over.
300 /// struct SolveCommand;
301 ///
302 /// impl CommandHandler for SolveCommand {
303 /// fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
304 /// Ok(())
305 /// }
306 ///
307 /// fn expected_fault_tolerance(&self) -> Option<bool> {
308 /// Some(false)
309 /// }
310 /// }
311 /// ```
312 fn expected_fault_tolerance(&self) -> Option<bool> {
313 None
314 }
315}
316
317/// Async counterpart of [`CommandHandler`].
318///
319/// Additive to `CommandHandler` — it does not replace it.
320/// Implementations use this trait when their command body needs to perform
321/// async I/O (network calls, streaming, etc.). The signatures deliberately
322/// mirror `CommandHandler` exactly, `execute`/`validate` aside from the
323/// `async` keyword, so that migrating a handler from sync to async is a
324/// mechanical change.
325///
326/// # Object Safety
327///
328/// Made `dyn`-compatible via `#[async_trait]` (which desugars `async fn` to
329/// a boxed, pinned future under the hood). Stored as `Box<dyn
330/// AsyncCommandHandler>` in the registry, exactly like `CommandHandler` is
331/// stored as `Box<dyn CommandHandler>`.
332///
333/// # Thread Safety
334///
335/// Same constraint as `CommandHandler`: `Send + Sync` is required so the
336/// handler can be shared across the registry and, transitively, across
337/// threads if the application needs it.
338///
339/// # Why a separate trait instead of an async `CommandHandler`?
340///
341/// Existing sync `CommandHandler` implementations (including downstream
342/// consumers) must keep compiling unchanged.
343///
344/// `dynamic-cli` does not depend on `tokio` or any other async runtime.
345/// The CLI and REPL dispatch loops are strictly sequential (each command
346/// runs to completion before the next input is read), so the dispatch
347/// site drives the returned future with `futures::executor::block_on`.
348/// The future borrows the execution context, so it could not be spawned
349/// onto a runtime anyway. A handler that needs a runtime, e.g. for
350/// network I/O, uses its own inside `execute`.
351///
352/// # Example
353///
354/// ```
355/// use async_trait::async_trait;
356/// use dynamic_cli::executor::{AsyncCommandHandler, ParsedArgs};
357/// use dynamic_cli::context::ExecutionContext;
358/// use dynamic_cli::error::ExecutionError;
359/// use dynamic_cli::Result;
360///
361/// struct FetchCommand;
362///
363/// #[async_trait]
364/// impl AsyncCommandHandler for FetchCommand {
365/// async fn execute(
366/// &self,
367/// _context: &mut dyn ExecutionContext,
368/// args: &ParsedArgs,
369/// ) -> Result<()> {
370/// let url = args.get_scalar("url").ok_or_else(|| {
371/// ExecutionError::CommandFailed(anyhow::anyhow!("Missing 'url' argument"))
372/// })?;
373/// // Real implementations would `.await` an async HTTP call here.
374/// println!("Fetching {url}...");
375/// Ok(())
376/// }
377///
378/// async fn validate(&self, args: &ParsedArgs) -> Result<()> {
379/// if args.get_scalar("url").is_none() {
380/// return Err(ExecutionError::CommandFailed(anyhow::anyhow!("url is required")).into());
381/// }
382/// Ok(())
383/// }
384/// }
385/// ```
386#[async_trait]
387pub trait AsyncCommandHandler: Send + Sync {
388 /// Async equivalent of [`CommandHandler::execute`]. Same contract:
389 /// receives the mutable execution context and the parsed arguments,
390 /// returns `Ok(())` on success or a `DynamicCliError` on failure.
391 async fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()>;
392
393 /// Async equivalent of [`CommandHandler::validate`]. Same contract and
394 /// same default (accepts all arguments) — override only for custom
395 /// validation logic.
396 async fn validate(&self, _args: &ParsedArgs) -> Result<()> {
397 Ok(())
398 }
399
400 /// Mirrors [`CommandHandler::expected_fault_tolerance`] exactly — same
401 /// contract, same default (`None`), checked by
402 /// `CommandRegistry::register_async` the same way `register_sync` checks
403 /// it for [`CommandHandler`].
404 ///
405 /// Deliberately **not** an `async fn`, unlike `execute`/`validate`
406 /// above: this method does no work of its own and is evaluated at
407 /// registration time, before any dispatch — sync or async — so making
408 /// it async would only force `register_async` (itself a synchronous
409 /// function) to `block_on` it for no benefit.
410 fn expected_fault_tolerance(&self) -> Option<bool> {
411 None
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use crate::error::ExecutionError;
419 use std::any::Any;
420 use std::collections::HashMap;
421 use std::sync::Arc;
422
423 /// Test helper: build a scalar-only `ParsedArgs` from `[(key, value), ...]`.
424 fn scalar_args<const N: usize>(pairs: [(&str, &str); N]) -> ParsedArgs {
425 let map: HashMap<String, String> = pairs
426 .into_iter()
427 .map(|(k, v)| (k.to_string(), v.to_string()))
428 .collect();
429 ParsedArgs::from_scalars(map)
430 }
431
432 // ============================================================================
433 // TEST FIXTURES
434 // ============================================================================
435
436 /// Simple test context for unit tests
437 #[derive(Default)]
438 struct TestContext {
439 state: String,
440 }
441
442 impl ExecutionContext for TestContext {
443 fn as_any(&self) -> &dyn Any {
444 self
445 }
446
447 fn as_any_mut(&mut self) -> &mut dyn Any {
448 self
449 }
450 }
451
452 /// Simple command that prints to context
453 struct HelloCommand;
454
455 impl CommandHandler for HelloCommand {
456 fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()> {
457 let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
458 ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
459 })?;
460
461 let name = args.get_scalar("name").unwrap_or("World");
462 ctx.state = format!("Hello, {}!", name);
463 Ok(())
464 }
465 }
466
467 /// Command with custom validation
468 struct ValidatedCommand;
469
470 impl CommandHandler for ValidatedCommand {
471 fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
472 Ok(())
473 }
474
475 fn validate(&self, args: &ParsedArgs) -> Result<()> {
476 // Require "count" argument to be present and > 0
477 if let Some(count) = args.get_scalar("count") {
478 let count_val: i32 = count.parse().map_err(|_| {
479 ExecutionError::CommandFailed(anyhow::anyhow!("count must be an integer"))
480 })?;
481
482 if count_val <= 0 {
483 return Err(ExecutionError::CommandFailed(anyhow::anyhow!(
484 "count must be positive"
485 ))
486 .into());
487 }
488 } else {
489 return Err(
490 ExecutionError::CommandFailed(anyhow::anyhow!("count is required")).into(),
491 );
492 }
493 Ok(())
494 }
495 }
496
497 /// Command that fails during execution
498 struct FailingCommand;
499
500 impl CommandHandler for FailingCommand {
501 fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
502 Err(ExecutionError::CommandFailed(anyhow::anyhow!("Simulated failure")).into())
503 }
504 }
505
506 /// Command that modifies context
507 struct StatefulCommand;
508
509 impl CommandHandler for StatefulCommand {
510 fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()> {
511 let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
512 ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
513 })?;
514
515 let value = args.get_scalar("value").unwrap_or("default");
516 ctx.state.push_str(value);
517 Ok(())
518 }
519 }
520
521 // ============================================================================
522 // BASIC FUNCTIONALITY TESTS
523 // ============================================================================
524
525 #[test]
526 fn test_basic_execution() {
527 let handler = HelloCommand;
528 let mut context = TestContext::default();
529 let args = scalar_args([("name", "Rust")]);
530
531 let result = handler.execute(&mut context, &args);
532
533 assert!(result.is_ok());
534 assert_eq!(context.state, "Hello, Rust!");
535 }
536
537 #[test]
538 fn test_execution_without_args() {
539 let handler = HelloCommand;
540 let mut context = TestContext::default();
541 let args = ParsedArgs::from_scalars(HashMap::new());
542
543 let result = handler.execute(&mut context, &args);
544
545 assert!(result.is_ok());
546 assert_eq!(context.state, "Hello, World!");
547 }
548
549 #[test]
550 fn test_execution_with_empty_name() {
551 let handler = HelloCommand;
552 let mut context = TestContext::default();
553 let args = scalar_args([("name", "")]);
554
555 let result = handler.execute(&mut context, &args);
556
557 assert!(result.is_ok());
558 assert_eq!(context.state, "Hello, !");
559 }
560
561 // ============================================================================
562 // VALIDATION TESTS
563 // ============================================================================
564
565 #[test]
566 fn test_default_validation_accepts_all() {
567 let handler = HelloCommand;
568 let args = scalar_args([("random", "value")]);
569
570 let result = handler.validate(&args);
571
572 assert!(result.is_ok());
573 }
574
575 #[test]
576 fn test_custom_validation_success() {
577 let handler = ValidatedCommand;
578 let args = scalar_args([("count", "5")]);
579
580 let result = handler.validate(&args);
581
582 assert!(result.is_ok());
583 }
584
585 #[test]
586 fn test_custom_validation_missing_arg() {
587 let handler = ValidatedCommand;
588 let args = ParsedArgs::from_scalars(HashMap::new());
589
590 let result = handler.validate(&args);
591
592 assert!(result.is_err());
593 let err_msg = format!("{}", result.unwrap_err());
594 assert!(err_msg.contains("required"));
595 }
596
597 #[test]
598 fn test_custom_validation_invalid_value() {
599 let handler = ValidatedCommand;
600 let args = scalar_args([("count", "0")]);
601
602 let result = handler.validate(&args);
603
604 assert!(result.is_err());
605 let err_msg = format!("{}", result.unwrap_err());
606 assert!(err_msg.contains("positive"));
607 }
608
609 #[test]
610 fn test_custom_validation_non_integer() {
611 let handler = ValidatedCommand;
612 let args = scalar_args([("count", "abc")]);
613
614 let result = handler.validate(&args);
615
616 assert!(result.is_err());
617 let err_msg = format!("{}", result.unwrap_err());
618 assert!(err_msg.contains("integer"));
619 }
620
621 // ============================================================================
622 // ERROR HANDLING TESTS
623 // ============================================================================
624
625 #[test]
626 fn test_execution_failure() {
627 let handler = FailingCommand;
628 let mut context = TestContext::default();
629 let args = ParsedArgs::from_scalars(HashMap::new());
630
631 let result = handler.execute(&mut context, &args);
632
633 assert!(result.is_err());
634 let err_msg = format!("{}", result.unwrap_err());
635 assert!(err_msg.contains("Simulated failure"));
636 }
637
638 #[test]
639 fn test_context_downcast_failure() {
640 // Use a different context type to trigger downcast failure
641 struct WrongContext;
642
643 impl ExecutionContext for WrongContext {
644 fn as_any(&self) -> &dyn Any {
645 self
646 }
647
648 fn as_any_mut(&mut self) -> &mut dyn Any {
649 self
650 }
651 }
652
653 let handler = HelloCommand;
654 let mut wrong_context = WrongContext;
655 let args = ParsedArgs::from_scalars(HashMap::new());
656
657 let result = handler.execute(&mut wrong_context, &args);
658
659 assert!(result.is_err());
660 let err_msg = format!("{}", result.unwrap_err());
661 assert!(err_msg.contains("Wrong context type"));
662 }
663
664 // ============================================================================
665 // STATE MODIFICATION TESTS
666 // ============================================================================
667
668 #[test]
669 fn test_context_state_modification() {
670 let handler = StatefulCommand;
671 let mut context = TestContext {
672 state: "initial".to_string(),
673 };
674 let args = scalar_args([("value", "_modified")]);
675
676 let result = handler.execute(&mut context, &args);
677
678 assert!(result.is_ok());
679 assert_eq!(context.state, "initial_modified");
680 }
681
682 #[test]
683 fn test_multiple_executions_preserve_state() {
684 let handler = StatefulCommand;
685 let mut context = TestContext::default();
686
687 // First execution
688 let args1 = scalar_args([("value", "first")]);
689 handler.execute(&mut context, &args1).unwrap();
690 assert_eq!(context.state, "first");
691
692 // Second execution
693 let args2 = scalar_args([("value", "_second")]);
694 handler.execute(&mut context, &args2).unwrap();
695 assert_eq!(context.state, "first_second");
696 }
697
698 // ============================================================================
699 // TRAIT OBJECT TESTS
700 // ============================================================================
701
702 #[test]
703 fn test_trait_object_usage() {
704 // Verify that CommandHandler can be used as a trait object
705 let handler: Box<dyn CommandHandler> = Box::new(HelloCommand);
706 let mut context = TestContext::default();
707 let args = scalar_args([("name", "TraitObject")]);
708
709 let result = handler.execute(&mut context, &args);
710
711 assert!(result.is_ok());
712 assert_eq!(context.state, "Hello, TraitObject!");
713 }
714
715 #[test]
716 fn test_multiple_trait_objects() {
717 // Store multiple handlers as trait objects
718 let handlers: Vec<Box<dyn CommandHandler>> =
719 vec![Box::new(HelloCommand), Box::new(StatefulCommand)];
720
721 let mut context = TestContext::default();
722
723 // Execute first handler
724 let args1 = scalar_args([("name", "First")]);
725 handlers[0].execute(&mut context, &args1).unwrap();
726 assert_eq!(context.state, "Hello, First!");
727
728 // Execute second handler
729 context.state.clear();
730 let args2 = scalar_args([("value", "Second")]);
731 handlers[1].execute(&mut context, &args2).unwrap();
732 assert_eq!(context.state, "Second");
733 }
734
735 // ============================================================================
736 // THREAD SAFETY TESTS
737 // ============================================================================
738
739 #[test]
740 fn test_send_sync_requirement() {
741 // This test verifies that CommandHandler is Send + Sync
742 // by using it in a multi-threaded context
743 let handler: Arc<dyn CommandHandler> = Arc::new(HelloCommand);
744
745 // Clone the Arc to simulate sharing across threads
746 let handler_clone = handler.clone();
747
748 // This compilation test ensures Send + Sync are satisfied
749 let _ = std::thread::spawn(move || {
750 let _h = handler_clone;
751 });
752 }
753
754 #[test]
755 fn test_concurrent_validation() {
756 // Test that validation can be called from multiple threads
757 let handler = Arc::new(ValidatedCommand);
758 let handler_clone = handler.clone();
759
760 let handle = std::thread::spawn(move || {
761 let args = scalar_args([("count", "10")]);
762 handler_clone.validate(&args)
763 });
764
765 let args = scalar_args([("count", "5")]);
766 let result1 = handler.validate(&args);
767
768 let result2 = handle.join().unwrap();
769
770 assert!(result1.is_ok());
771 assert!(result2.is_ok());
772 }
773
774 // ============================================================================
775 // EDGE CASES
776 // ============================================================================
777
778 #[test]
779 fn test_empty_args() {
780 let handler = StatefulCommand;
781 let mut context = TestContext::default();
782 let args = ParsedArgs::from_scalars(HashMap::new());
783
784 // Should use default value
785 let result = handler.execute(&mut context, &args);
786
787 assert!(result.is_ok());
788 assert_eq!(context.state, "default");
789 }
790
791 #[test]
792 fn test_args_with_special_characters() {
793 let handler = HelloCommand;
794 let mut context = TestContext::default();
795 let args = scalar_args([("name", "Hello, 世界! 🌍")]);
796
797 let result = handler.execute(&mut context, &args);
798
799 assert!(result.is_ok());
800 assert_eq!(context.state, "Hello, Hello, 世界! 🌍!");
801 }
802
803 #[test]
804 fn test_very_long_argument() {
805 let handler = HelloCommand;
806 let mut context = TestContext::default();
807 let long_name = "x".repeat(10000);
808 let args = scalar_args([("name", long_name.as_str())]);
809
810 let result = handler.execute(&mut context, &args);
811
812 assert!(result.is_ok());
813 assert!(context.state.contains(&long_name));
814 }
815
816 // ============================================================================
817 // SHARED STATE TESTS
818 // ============================================================================
819
820 #[test]
821 fn test_shared_mutable_context() {
822 // Test that context can be safely modified by multiple commands
823 let handler1 = StatefulCommand;
824 let handler2 = StatefulCommand;
825 let mut context = TestContext::default();
826
827 let args1 = scalar_args([("value", "A")]);
828 handler1.execute(&mut context, &args1).unwrap();
829
830 let args2 = scalar_args([("value", "B")]);
831 handler2.execute(&mut context, &args2).unwrap();
832
833 assert_eq!(context.state, "AB");
834 }
835
836 // Test to ensure the trait is indeed object-safe at compile time
837 #[test]
838 fn test_object_safety_compile_time() {
839 // This function signature requires CommandHandler to be object-safe
840 fn _accepts_trait_object(_: &dyn CommandHandler) {}
841
842 // If this compiles, the trait is object-safe
843 let handler = HelloCommand;
844 _accepts_trait_object(&handler);
845 }
846
847 // Test that demonstrates why we can't have generic methods
848 // (This is a documentation test, not an actual test that runs)
849 /// ```compile_fail
850 /// use dynamic_cli::executor::CommandHandler;
851 ///
852 /// trait BrokenHandler: CommandHandler {
853 /// fn generic_method<T>(&self, value: T);
854 /// }
855 ///
856 /// // This would fail because trait objects can't have generic methods
857 /// fn use_as_trait_object(handler: &dyn BrokenHandler) {
858 /// // Cannot call generic_method on trait object
859 /// }
860 /// ```
861 #[allow(dead_code)]
862 fn test_no_generic_methods_documentation() {}
863
864 // ============================================================================
865 // expected_fault_tolerance() TESTS — DD-028 (#64)
866 // ============================================================================
867
868 /// A handler whose author never expressed an opinion — the common case.
869 struct FaultAgnosticCommand;
870
871 impl CommandHandler for FaultAgnosticCommand {
872 fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
873 Ok(())
874 }
875 }
876
877 /// A handler whose author declares its failures must never be ignored.
878 struct FaultIntolerantCommand;
879
880 impl CommandHandler for FaultIntolerantCommand {
881 fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
882 Ok(())
883 }
884
885 fn expected_fault_tolerance(&self) -> Option<bool> {
886 Some(false)
887 }
888 }
889
890 /// A handler whose author declares its failures are safe to ignore.
891 struct FaultTolerantCommand;
892
893 impl CommandHandler for FaultTolerantCommand {
894 fn execute(&self, _context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
895 Ok(())
896 }
897
898 fn expected_fault_tolerance(&self) -> Option<bool> {
899 Some(true)
900 }
901 }
902
903 #[test]
904 fn test_expected_fault_tolerance_defaults_to_none() {
905 // Every pre-existing handler written before DD-028 (#64) keeps
906 // compiling and behaving exactly as before — no supertrait, no
907 // required impl, nothing to add.
908 let handler = FaultAgnosticCommand;
909 assert_eq!(handler.expected_fault_tolerance(), None);
910
911 // HelloCommand and ValidatedCommand, defined before DD-028 (#64),
912 // are just as unaffected.
913 assert_eq!(HelloCommand.expected_fault_tolerance(), None);
914 assert_eq!(ValidatedCommand.expected_fault_tolerance(), None);
915 }
916
917 #[test]
918 fn test_expected_fault_tolerance_override() {
919 assert_eq!(
920 FaultIntolerantCommand.expected_fault_tolerance(),
921 Some(false)
922 );
923 assert_eq!(FaultTolerantCommand.expected_fault_tolerance(), Some(true));
924 }
925
926 #[test]
927 fn test_expected_fault_tolerance_via_trait_object() {
928 // The method must be reachable through `Box<dyn CommandHandler>`,
929 // exactly like `execute`/`validate` — this is what
930 // `CommandRegistry::register_sync` actually calls it through.
931 let handler: Box<dyn CommandHandler> = Box::new(FaultIntolerantCommand);
932 assert_eq!(handler.expected_fault_tolerance(), Some(false));
933 }
934
935 // ============================================================================
936 // AsyncCommandHandler TESTS — DA-022 (#8)
937 // ============================================================================
938
939 /// Async command that writes to the test context, mirroring `HelloCommand`.
940 struct AsyncHelloCommand;
941
942 #[async_trait]
943 impl AsyncCommandHandler for AsyncHelloCommand {
944 async fn execute(
945 &self,
946 context: &mut dyn ExecutionContext,
947 args: &ParsedArgs,
948 ) -> Result<()> {
949 let ctx = crate::context::downcast_mut::<TestContext>(context).ok_or_else(|| {
950 ExecutionError::CommandFailed(anyhow::anyhow!("Wrong context type"))
951 })?;
952 let name = args.get_scalar("name").unwrap_or("World");
953 ctx.state = format!("Hello, {}!", name);
954 Ok(())
955 }
956 }
957
958 /// Async command with custom validation, mirroring `ValidatedCommand`.
959 struct AsyncValidatedCommand;
960
961 #[async_trait]
962 impl AsyncCommandHandler for AsyncValidatedCommand {
963 async fn execute(
964 &self,
965 _context: &mut dyn ExecutionContext,
966 _args: &ParsedArgs,
967 ) -> Result<()> {
968 Ok(())
969 }
970
971 async fn validate(&self, args: &ParsedArgs) -> Result<()> {
972 if args.get_scalar("count").is_none() {
973 return Err(
974 ExecutionError::CommandFailed(anyhow::anyhow!("count is required")).into(),
975 );
976 }
977 Ok(())
978 }
979 }
980
981 /// Async command that fails during execution, mirroring `FailingCommand`.
982 struct AsyncFailingCommand;
983
984 #[async_trait]
985 impl AsyncCommandHandler for AsyncFailingCommand {
986 async fn execute(
987 &self,
988 _context: &mut dyn ExecutionContext,
989 _args: &ParsedArgs,
990 ) -> Result<()> {
991 Err(ExecutionError::CommandFailed(anyhow::anyhow!("Simulated async failure")).into())
992 }
993 }
994
995 #[test]
996 fn test_async_basic_execution() {
997 let handler = AsyncHelloCommand;
998 let mut context = TestContext::default();
999 let args = scalar_args([("name", "Rust")]);
1000
1001 let result = futures::executor::block_on(handler.execute(&mut context, &args));
1002
1003 assert!(result.is_ok());
1004 assert_eq!(context.state, "Hello, Rust!");
1005 }
1006
1007 #[test]
1008 fn test_async_default_validation_accepts_all() {
1009 let handler = AsyncHelloCommand;
1010 let args = scalar_args([("random", "value")]);
1011
1012 let result = futures::executor::block_on(handler.validate(&args));
1013
1014 assert!(result.is_ok());
1015 }
1016
1017 #[test]
1018 fn test_async_custom_validation_missing_arg() {
1019 let handler = AsyncValidatedCommand;
1020 let args = ParsedArgs::from_scalars(HashMap::new());
1021
1022 let result = futures::executor::block_on(handler.validate(&args));
1023
1024 assert!(result.is_err());
1025 let err_msg = format!("{}", result.unwrap_err());
1026 assert!(err_msg.contains("required"));
1027 }
1028
1029 #[test]
1030 fn test_async_custom_validation_success() {
1031 let handler = AsyncValidatedCommand;
1032 let args = scalar_args([("count", "5")]);
1033
1034 let result = futures::executor::block_on(handler.validate(&args));
1035
1036 assert!(result.is_ok());
1037 }
1038
1039 #[test]
1040 fn test_async_execution_failure() {
1041 let handler = AsyncFailingCommand;
1042 let mut context = TestContext::default();
1043 let args = ParsedArgs::from_scalars(HashMap::new());
1044
1045 let result = futures::executor::block_on(handler.execute(&mut context, &args));
1046
1047 assert!(result.is_err());
1048 let err_msg = format!("{}", result.unwrap_err());
1049 assert!(err_msg.contains("Simulated async failure"));
1050 }
1051
1052 #[test]
1053 fn test_async_trait_object_usage() {
1054 // Verify that AsyncCommandHandler can be used as a trait object —
1055 // the core object-safety guarantee DA-022 (#8) depends on.
1056 let handler: Box<dyn AsyncCommandHandler> = Box::new(AsyncHelloCommand);
1057 let mut context = TestContext::default();
1058 let args = scalar_args([("name", "TraitObject")]);
1059
1060 let result = futures::executor::block_on(handler.execute(&mut context, &args));
1061
1062 assert!(result.is_ok());
1063 assert_eq!(context.state, "Hello, TraitObject!");
1064 }
1065
1066 #[test]
1067 fn test_async_send_sync_requirement() {
1068 // Verifies AsyncCommandHandler is Send + Sync by sharing it across
1069 // threads via Arc — same pattern as test_send_sync_requirement above.
1070 let handler: Arc<dyn AsyncCommandHandler> = Arc::new(AsyncHelloCommand);
1071 let handler_clone = handler.clone();
1072
1073 let _ = std::thread::spawn(move || {
1074 let _h = handler_clone;
1075 });
1076 }
1077
1078 #[test]
1079 fn test_async_object_safety_compile_time() {
1080 // If this compiles, AsyncCommandHandler is dyn-compatible.
1081 fn _accepts_trait_object(_: &dyn AsyncCommandHandler) {}
1082 _accepts_trait_object(&AsyncHelloCommand);
1083 }
1084
1085 // ============================================================================
1086 // expected_fault_tolerance() TESTS — async mirror, DD-028 (#64)
1087 // ============================================================================
1088
1089 /// Async handler whose author declares its failures must never be
1090 /// ignored — the async mirror of `FaultIntolerantCommand`.
1091 struct AsyncFaultIntolerantCommand;
1092
1093 #[async_trait]
1094 impl AsyncCommandHandler for AsyncFaultIntolerantCommand {
1095 async fn execute(
1096 &self,
1097 _context: &mut dyn ExecutionContext,
1098 _args: &ParsedArgs,
1099 ) -> Result<()> {
1100 Ok(())
1101 }
1102
1103 fn expected_fault_tolerance(&self) -> Option<bool> {
1104 Some(false)
1105 }
1106 }
1107
1108 #[test]
1109 fn test_async_expected_fault_tolerance_defaults_to_none() {
1110 // AsyncHelloCommand, defined before DD-028 (#64), keeps compiling
1111 // and behaving exactly as before — no supertrait, nothing to add.
1112 assert_eq!(AsyncHelloCommand.expected_fault_tolerance(), None);
1113 }
1114
1115 #[test]
1116 fn test_async_expected_fault_tolerance_override() {
1117 assert_eq!(
1118 AsyncFaultIntolerantCommand.expected_fault_tolerance(),
1119 Some(false)
1120 );
1121 }
1122
1123 #[test]
1124 fn test_async_expected_fault_tolerance_via_trait_object() {
1125 // Reachable through `Box<dyn AsyncCommandHandler>` — what
1126 // `CommandRegistry::register_async` actually calls it through.
1127 let handler: Box<dyn AsyncCommandHandler> = Box::new(AsyncFaultIntolerantCommand);
1128 assert_eq!(handler.expected_fault_tolerance(), Some(false));
1129 }
1130}