standout_dispatch/handler.rs
1//! Command handler types.
2//!
3//! This module provides the core types for building logic handlers - the
4//! business logic layer in the dispatch pipeline.
5//!
6//! # Design Rationale
7//!
8//! Logic handlers are responsible for business logic only. They:
9//!
10//! - Receive parsed CLI arguments (`&ArgMatches`) and execution context
11//! - Perform application logic (database queries, file operations, etc.)
12//! - Return serializable data that will be passed to the render handler
13//!
14//! Handlers explicitly do not handle:
15//! - Output formatting (that's the render handler's job)
16//! - Template selection (that's configured at the framework level)
17//! - Theme/style decisions (that's the render handler's job)
18//!
19//! This separation keeps handlers focused and testable - you can unit test
20//! a handler by checking the data it returns, without worrying about rendering.
21//!
22//! # State Management: App State vs Extensions
23//!
24//! [`CommandContext`] provides two mechanisms for state injection:
25//!
26//! | Field | Mutability | Lifetime | Purpose |
27//! |-------|------------|----------|---------|
28//! | `app_state` | Immutable (`&`) | App lifetime (shared via Arc) | Database, Config, API clients |
29//! | `extensions` | Mutable (`&mut`) | Request lifetime | Per-request state, user scope |
30//!
31//! **App State** is configured at app build time via `AppBuilder::app_state()` and shared
32//! immutably across all command invocations. Use for long-lived resources:
33//!
34//! ```rust,ignore
35//! // At app build time
36//! App::builder()
37//! .app_state(Database::connect()?)
38//! .app_state(Config::load()?)
39//! .build()?
40//!
41//! // In handlers
42//! fn list_handler(matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Vec<User>> {
43//! let db = ctx.app_state.get_required::<Database>()?;
44//! Ok(Output::Render(db.list_users()?))
45//! }
46//! ```
47//!
48//! **Extensions** are injected per-request by pre-dispatch hooks. Use for request-scoped data:
49//!
50//! ```rust,ignore
51//! Hooks::new().pre_dispatch(|matches, ctx| {
52//! let user_id = matches.get_one::<String>("user").unwrap();
53//! ctx.extensions.insert(UserScope { user_id: user_id.clone() });
54//! Ok(())
55//! })
56//! ```
57//!
58//! # Core Types
59//!
60//! - [`CommandContext`]: Environment information passed to handlers
61//! - [`Extensions`]: Type-safe container for injecting custom state
62//! - [`Output`]: What a handler produces (render data, silent, or binary)
63//! - [`HandlerResult`]: The result type for handlers (`Result<Output<T>, Error>`)
64//! - [`RunResult`]: The result of running the CLI dispatcher
65//! - [`Handler`]: Trait for thread-safe command handlers (`Send + Sync`, `&self`)
66//! - [`LocalHandler`]: Trait for local command handlers (no `Send + Sync`, `&mut self`)
67
68use clap::ArgMatches;
69use serde::Serialize;
70use std::any::{Any, TypeId};
71use std::collections::HashMap;
72use std::fmt;
73use std::sync::Arc;
74
75/// Type-safe container for injecting custom state into handlers.
76///
77/// Extensions allow pre-dispatch hooks to inject state that handlers can retrieve.
78/// This enables dependency injection without modifying handler signatures.
79///
80/// # Warning: Clone Behavior
81///
82/// `Extensions` is **not** cloned when the container is cloned. Cloning an `Extensions` instance
83/// results in a new, empty map. This is because the underlying `Box<dyn Any>` values cannot
84/// be cloned generically.
85///
86/// If you need to share state across threads/clones, use `Arc<T>` inside the extension.
87///
88/// # Example
89///
90/// ```rust
91/// use standout_dispatch::{Extensions, CommandContext};
92///
93/// // Define your state types
94/// struct ApiClient { base_url: String }
95/// struct UserScope { user_id: u64 }
96///
97/// // In a pre-dispatch hook, inject state
98/// let mut ctx = CommandContext::default();
99/// ctx.extensions.insert(ApiClient { base_url: "https://api.example.com".into() });
100/// ctx.extensions.insert(UserScope { user_id: 42 });
101///
102/// // In a handler, retrieve state
103/// let api = ctx.extensions.get_required::<ApiClient>()?;
104/// println!("API base: {}", api.base_url);
105/// # Ok::<(), anyhow::Error>(())
106/// ```
107#[derive(Default)]
108pub struct Extensions {
109 map: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
110}
111
112impl Extensions {
113 /// Creates a new empty extensions container.
114 pub fn new() -> Self {
115 Self::default()
116 }
117
118 /// Inserts a value into the extensions.
119 ///
120 /// If a value of this type already exists, it is replaced and returned.
121 pub fn insert<T: Send + Sync + 'static>(&mut self, val: T) -> Option<T> {
122 self.map
123 .insert(TypeId::of::<T>(), Box::new(val))
124 .and_then(|boxed| boxed.downcast().ok().map(|b| *b))
125 }
126
127 /// Gets a reference to a value of the specified type.
128 ///
129 /// Returns `None` if no value of this type exists.
130 pub fn get<T: 'static>(&self) -> Option<&T> {
131 self.map
132 .get(&TypeId::of::<T>())
133 .and_then(|boxed| boxed.downcast_ref())
134 }
135
136 /// Gets a mutable reference to a value of the specified type.
137 ///
138 /// Returns `None` if no value of this type exists.
139 pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
140 self.map
141 .get_mut(&TypeId::of::<T>())
142 .and_then(|boxed| boxed.downcast_mut())
143 }
144
145 /// Gets a required reference to a value of the specified type.
146 ///
147 /// Returns an error if no value of this type exists.
148 pub fn get_required<T: 'static>(&self) -> Result<&T, anyhow::Error> {
149 self.get::<T>().ok_or_else(|| {
150 anyhow::anyhow!(
151 "Extension missing: type {} not found in context",
152 std::any::type_name::<T>()
153 )
154 })
155 }
156
157 /// Gets a required mutable reference to a value of the specified type.
158 ///
159 /// Returns an error if no value of this type exists.
160 pub fn get_mut_required<T: 'static>(&mut self) -> Result<&mut T, anyhow::Error> {
161 self.get_mut::<T>().ok_or_else(|| {
162 anyhow::anyhow!(
163 "Extension missing: type {} not found in context",
164 std::any::type_name::<T>()
165 )
166 })
167 }
168
169 /// Removes a value of the specified type, returning it if it existed.
170 pub fn remove<T: 'static>(&mut self) -> Option<T> {
171 self.map
172 .remove(&TypeId::of::<T>())
173 .and_then(|boxed| boxed.downcast().ok().map(|b| *b))
174 }
175
176 /// Returns `true` if the extensions contain a value of the specified type.
177 pub fn contains<T: 'static>(&self) -> bool {
178 self.map.contains_key(&TypeId::of::<T>())
179 }
180
181 /// Returns the number of extensions stored.
182 pub fn len(&self) -> usize {
183 self.map.len()
184 }
185
186 /// Returns `true` if no extensions are stored.
187 pub fn is_empty(&self) -> bool {
188 self.map.is_empty()
189 }
190
191 /// Removes all extensions.
192 pub fn clear(&mut self) {
193 self.map.clear();
194 }
195}
196
197impl fmt::Debug for Extensions {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 f.debug_struct("Extensions")
200 .field("len", &self.map.len())
201 .finish_non_exhaustive()
202 }
203}
204
205impl Clone for Extensions {
206 fn clone(&self) -> Self {
207 // Extensions cannot be cloned because Box<dyn Any> isn't Clone.
208 // Return empty extensions on clone - this is a limitation but
209 // matches the behavior of http::Extensions.
210 Self::new()
211 }
212}
213
214/// Context passed to command handlers.
215///
216/// Provides information about the execution environment plus two mechanisms
217/// for state injection:
218///
219/// - **`app_state`**: Immutable, app-lifetime state (Database, Config, API clients)
220/// - **`extensions`**: Mutable, per-request state (UserScope, RequestId)
221///
222/// Note that output format is deliberately not included here - format decisions
223/// are made by the render handler, not by logic handlers.
224///
225/// # App State (Immutable, Shared)
226///
227/// App state is configured at build time and shared across all dispatches:
228///
229/// ```rust,ignore
230/// use standout::cli::App;
231///
232/// struct Database { /* ... */ }
233/// struct Config { api_url: String }
234///
235/// App::builder()
236/// .app_state(Database::connect()?)
237/// .app_state(Config { api_url: "https://api.example.com".into() })
238/// .command("list", list_handler, "{{ items }}")
239/// .build()?
240/// ```
241///
242/// Handlers retrieve app state immutably:
243///
244/// ```rust,ignore
245/// fn list_handler(matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Vec<Item>> {
246/// let db = ctx.app_state.get_required::<Database>()?;
247/// let config = ctx.app_state.get_required::<Config>()?;
248/// Ok(Output::Render(db.list_items(&config.api_url)?))
249/// }
250/// ```
251///
252/// ## Shared Mutable State
253///
254/// Since `app_state` is shared via `Arc`, it is immutable by default. To share mutable state
255/// (like counters or caches), use interior mutability primitives like `RwLock`, `Mutex`, or atomic types:
256///
257/// ```rust,ignore
258/// use std::sync::atomic::AtomicUsize;
259///
260/// struct Metrics { request_count: AtomicUsize }
261///
262/// // Builder
263/// App::builder().app_state(Metrics { request_count: AtomicUsize::new(0) });
264///
265/// // Handler
266/// let metrics = ctx.app_state.get_required::<Metrics>()?;
267/// metrics.request_count.fetch_add(1, Ordering::Relaxed);
268/// ```
269///
270/// # Extensions (Mutable, Per-Request)
271///
272/// Pre-dispatch hooks inject per-request state into `extensions`:
273///
274/// ```rust
275/// use standout_dispatch::{Hooks, HookError, CommandContext};
276///
277/// struct UserScope { user_id: String }
278///
279/// let hooks = Hooks::new()
280/// .pre_dispatch(|matches, ctx| {
281/// let user_id = matches.get_one::<String>("user").unwrap();
282/// ctx.extensions.insert(UserScope { user_id: user_id.clone() });
283/// Ok(())
284/// });
285///
286/// // In handler:
287/// fn my_handler(matches: &clap::ArgMatches, ctx: &CommandContext) -> anyhow::Result<()> {
288/// let scope = ctx.extensions.get_required::<UserScope>()?;
289/// // use scope.user_id...
290/// Ok(())
291/// }
292/// ```
293#[derive(Debug)]
294pub struct CommandContext {
295 /// The command path being executed (e.g., ["config", "get"])
296 pub command_path: Vec<String>,
297
298 /// Immutable app-level state shared across all dispatches.
299 ///
300 /// Configured via `AppBuilder::app_state()`. Contains long-lived resources
301 /// like database connections, configuration, and API clients.
302 ///
303 /// Use `get::<T>()` or `get_required::<T>()` to retrieve values.
304 pub app_state: Arc<Extensions>,
305
306 /// Mutable per-request state container.
307 ///
308 /// Pre-dispatch hooks can insert values that handlers retrieve.
309 /// Each dispatch gets a fresh Extensions instance.
310 pub extensions: Extensions,
311}
312
313impl CommandContext {
314 /// Creates a new CommandContext with the given path and shared app state.
315 ///
316 /// This is more efficient than `Default::default()` when you already have app_state.
317 pub fn new(command_path: Vec<String>, app_state: Arc<Extensions>) -> Self {
318 Self {
319 command_path,
320 app_state,
321 extensions: Extensions::new(),
322 }
323 }
324}
325
326impl Default for CommandContext {
327 fn default() -> Self {
328 Self {
329 command_path: Vec::new(),
330 app_state: Arc::new(Extensions::new()),
331 extensions: Extensions::new(),
332 }
333 }
334}
335
336/// What a handler produces.
337///
338/// This enum represents the different types of output a command handler can produce.
339#[derive(Debug)]
340pub enum Output<T: Serialize> {
341 /// Data to render with a template or serialize to JSON/YAML/etc.
342 Render(T),
343 /// Silent exit (no output produced)
344 Silent,
345 /// Binary output for file exports
346 Binary {
347 /// The binary data
348 data: Vec<u8>,
349 /// Suggested filename for the output
350 filename: String,
351 },
352}
353
354impl<T: Serialize> Output<T> {
355 /// Returns true if this is a render result.
356 pub fn is_render(&self) -> bool {
357 matches!(self, Output::Render(_))
358 }
359
360 /// Returns true if this is a silent result.
361 pub fn is_silent(&self) -> bool {
362 matches!(self, Output::Silent)
363 }
364
365 /// Returns true if this is a binary result.
366 pub fn is_binary(&self) -> bool {
367 matches!(self, Output::Binary { .. })
368 }
369}
370
371/// The result type for command handlers.
372///
373/// Enables use of the `?` operator for error propagation.
374pub type HandlerResult<T> = Result<Output<T>, anyhow::Error>;
375
376/// Result of running the CLI dispatcher.
377///
378/// After processing arguments, the dispatcher either handles a command
379/// or falls through for manual handling.
380#[derive(Debug)]
381pub enum RunResult {
382 /// A handler processed the command; contains the rendered output
383 Handled(String),
384 /// A handler produced binary output (bytes, suggested filename)
385 Binary(Vec<u8>, String),
386 /// Silent output (handler completed but produced no output)
387 Silent,
388 /// No handler matched; contains the ArgMatches for manual handling
389 NoMatch(ArgMatches),
390}
391
392impl RunResult {
393 /// Returns true if a handler processed the command (text output).
394 pub fn is_handled(&self) -> bool {
395 matches!(self, RunResult::Handled(_))
396 }
397
398 /// Returns true if the result is binary output.
399 pub fn is_binary(&self) -> bool {
400 matches!(self, RunResult::Binary(_, _))
401 }
402
403 /// Returns true if the result is silent.
404 pub fn is_silent(&self) -> bool {
405 matches!(self, RunResult::Silent)
406 }
407
408 /// Returns the output if handled, or None otherwise.
409 pub fn output(&self) -> Option<&str> {
410 match self {
411 RunResult::Handled(s) => Some(s),
412 _ => None,
413 }
414 }
415
416 /// Returns the binary data and filename if binary, or None otherwise.
417 pub fn binary(&self) -> Option<(&[u8], &str)> {
418 match self {
419 RunResult::Binary(bytes, filename) => Some((bytes, filename)),
420 _ => None,
421 }
422 }
423
424 /// Returns the matches if unhandled, or None if handled.
425 pub fn matches(&self) -> Option<&ArgMatches> {
426 match self {
427 RunResult::NoMatch(m) => Some(m),
428 _ => None,
429 }
430 }
431}
432
433/// Trait for thread-safe command handlers.
434///
435/// Handlers must be `Send + Sync` and use immutable `&self`.
436pub trait Handler: Send + Sync {
437 /// The output type produced by this handler (must be serializable)
438 type Output: Serialize;
439
440 /// Execute the handler with the given matches and context.
441 fn handle(&self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Self::Output>;
442}
443
444/// A wrapper that implements Handler for closures.
445pub struct FnHandler<F, T>
446where
447 F: Fn(&ArgMatches, &CommandContext) -> HandlerResult<T> + Send + Sync,
448 T: Serialize + Send + Sync,
449{
450 f: F,
451 _phantom: std::marker::PhantomData<fn() -> T>,
452}
453
454impl<F, T> FnHandler<F, T>
455where
456 F: Fn(&ArgMatches, &CommandContext) -> HandlerResult<T> + Send + Sync,
457 T: Serialize + Send + Sync,
458{
459 /// Creates a new FnHandler wrapping the given closure.
460 pub fn new(f: F) -> Self {
461 Self {
462 f,
463 _phantom: std::marker::PhantomData,
464 }
465 }
466}
467
468impl<F, T> Handler for FnHandler<F, T>
469where
470 F: Fn(&ArgMatches, &CommandContext) -> HandlerResult<T> + Send + Sync,
471 T: Serialize + Send + Sync,
472{
473 type Output = T;
474
475 fn handle(&self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<T> {
476 (self.f)(matches, ctx)
477 }
478}
479
480/// Trait for local (single-threaded) command handlers.
481///
482/// Unlike [`Handler`], this trait:
483/// - Does NOT require `Send + Sync`
484/// - Takes `&mut self` instead of `&self`
485/// - Allows handlers to mutate their internal state directly
486pub trait LocalHandler {
487 /// The output type produced by this handler (must be serializable)
488 type Output: Serialize;
489
490 /// Execute the handler with the given matches and context.
491 fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext)
492 -> HandlerResult<Self::Output>;
493}
494
495/// A wrapper that implements LocalHandler for FnMut closures.
496pub struct LocalFnHandler<F, T>
497where
498 F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T>,
499 T: Serialize,
500{
501 f: F,
502 _phantom: std::marker::PhantomData<fn() -> T>,
503}
504
505impl<F, T> LocalFnHandler<F, T>
506where
507 F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T>,
508 T: Serialize,
509{
510 /// Creates a new LocalFnHandler wrapping the given FnMut closure.
511 pub fn new(f: F) -> Self {
512 Self {
513 f,
514 _phantom: std::marker::PhantomData,
515 }
516 }
517}
518
519impl<F, T> LocalHandler for LocalFnHandler<F, T>
520where
521 F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T>,
522 T: Serialize,
523{
524 type Output = T;
525
526 fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<T> {
527 (self.f)(matches, ctx)
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use serde_json::json;
535
536 #[test]
537 fn test_command_context_creation() {
538 let ctx = CommandContext {
539 command_path: vec!["config".into(), "get".into()],
540 app_state: Arc::new(Extensions::new()),
541 extensions: Extensions::new(),
542 };
543 assert_eq!(ctx.command_path, vec!["config", "get"]);
544 }
545
546 #[test]
547 fn test_command_context_default() {
548 let ctx = CommandContext::default();
549 assert!(ctx.command_path.is_empty());
550 assert!(ctx.extensions.is_empty());
551 assert!(ctx.app_state.is_empty());
552 }
553
554 #[test]
555 fn test_command_context_with_app_state() {
556 struct Database {
557 url: String,
558 }
559 struct Config {
560 debug: bool,
561 }
562
563 // Build app state
564 let mut app_state = Extensions::new();
565 app_state.insert(Database {
566 url: "postgres://localhost".into(),
567 });
568 app_state.insert(Config { debug: true });
569 let app_state = Arc::new(app_state);
570
571 // Create context with app state
572 let ctx = CommandContext {
573 command_path: vec!["list".into()],
574 app_state: app_state.clone(),
575 extensions: Extensions::new(),
576 };
577
578 // Retrieve app state
579 let db = ctx.app_state.get::<Database>().unwrap();
580 assert_eq!(db.url, "postgres://localhost");
581
582 let config = ctx.app_state.get::<Config>().unwrap();
583 assert!(config.debug);
584
585 // App state is shared via Arc
586 assert_eq!(Arc::strong_count(&ctx.app_state), 2);
587 }
588
589 #[test]
590 fn test_command_context_app_state_get_required() {
591 struct Present;
592
593 let mut app_state = Extensions::new();
594 app_state.insert(Present);
595
596 let ctx = CommandContext {
597 command_path: vec![],
598 app_state: Arc::new(app_state),
599 extensions: Extensions::new(),
600 };
601
602 // Success case
603 assert!(ctx.app_state.get_required::<Present>().is_ok());
604
605 // Failure case
606 #[derive(Debug)]
607 struct Missing;
608 let err = ctx.app_state.get_required::<Missing>();
609 assert!(err.is_err());
610 assert!(err.unwrap_err().to_string().contains("Extension missing"));
611 }
612
613 // Extensions tests
614 #[test]
615 fn test_extensions_insert_and_get() {
616 struct MyState {
617 value: i32,
618 }
619
620 let mut ext = Extensions::new();
621 assert!(ext.is_empty());
622
623 ext.insert(MyState { value: 42 });
624 assert!(!ext.is_empty());
625 assert_eq!(ext.len(), 1);
626
627 let state = ext.get::<MyState>().unwrap();
628 assert_eq!(state.value, 42);
629 }
630
631 #[test]
632 fn test_extensions_get_mut() {
633 struct Counter {
634 count: i32,
635 }
636
637 let mut ext = Extensions::new();
638 ext.insert(Counter { count: 0 });
639
640 if let Some(counter) = ext.get_mut::<Counter>() {
641 counter.count += 1;
642 }
643
644 assert_eq!(ext.get::<Counter>().unwrap().count, 1);
645 }
646
647 #[test]
648 fn test_extensions_multiple_types() {
649 struct TypeA(i32);
650 struct TypeB(String);
651
652 let mut ext = Extensions::new();
653 ext.insert(TypeA(1));
654 ext.insert(TypeB("hello".into()));
655
656 assert_eq!(ext.len(), 2);
657 assert_eq!(ext.get::<TypeA>().unwrap().0, 1);
658 assert_eq!(ext.get::<TypeB>().unwrap().0, "hello");
659 }
660
661 #[test]
662 fn test_extensions_replace() {
663 struct Value(i32);
664
665 let mut ext = Extensions::new();
666 ext.insert(Value(1));
667
668 let old = ext.insert(Value(2));
669 assert_eq!(old.unwrap().0, 1);
670 assert_eq!(ext.get::<Value>().unwrap().0, 2);
671 }
672
673 #[test]
674 fn test_extensions_remove() {
675 struct Value(i32);
676
677 let mut ext = Extensions::new();
678 ext.insert(Value(42));
679
680 let removed = ext.remove::<Value>();
681 assert_eq!(removed.unwrap().0, 42);
682 assert!(ext.is_empty());
683 assert!(ext.get::<Value>().is_none());
684 }
685
686 #[test]
687 fn test_extensions_contains() {
688 struct Present;
689 struct Absent;
690
691 let mut ext = Extensions::new();
692 ext.insert(Present);
693
694 assert!(ext.contains::<Present>());
695 assert!(!ext.contains::<Absent>());
696 }
697
698 #[test]
699 fn test_extensions_clear() {
700 struct A;
701 struct B;
702
703 let mut ext = Extensions::new();
704 ext.insert(A);
705 ext.insert(B);
706 assert_eq!(ext.len(), 2);
707
708 ext.clear();
709 assert!(ext.is_empty());
710 }
711
712 #[test]
713 fn test_extensions_missing_type_returns_none() {
714 struct NotInserted;
715
716 let ext = Extensions::new();
717 assert!(ext.get::<NotInserted>().is_none());
718 }
719
720 #[test]
721 fn test_extensions_get_required() {
722 #[derive(Debug)]
723 struct Config {
724 value: i32,
725 }
726
727 let mut ext = Extensions::new();
728 ext.insert(Config { value: 100 });
729
730 // Success case
731 let val = ext.get_required::<Config>();
732 assert!(val.is_ok());
733 assert_eq!(val.unwrap().value, 100);
734
735 // Failure case
736 #[derive(Debug)]
737 struct Missing;
738 let err = ext.get_required::<Missing>();
739 assert!(err.is_err());
740 assert!(err
741 .unwrap_err()
742 .to_string()
743 .contains("Extension missing: type"));
744 }
745
746 #[test]
747 fn test_extensions_get_mut_required() {
748 #[derive(Debug)]
749 struct State {
750 count: i32,
751 }
752
753 let mut ext = Extensions::new();
754 ext.insert(State { count: 0 });
755
756 // Success case
757 {
758 let val = ext.get_mut_required::<State>();
759 assert!(val.is_ok());
760 val.unwrap().count += 1;
761 }
762 assert_eq!(ext.get_required::<State>().unwrap().count, 1);
763
764 // Failure case
765 #[derive(Debug)]
766 struct Missing;
767 let err = ext.get_mut_required::<Missing>();
768 assert!(err.is_err());
769 }
770
771 #[test]
772 fn test_extensions_clone_behavior() {
773 // Verify the documented behavior that Clone drops extensions
774 struct Data(i32);
775
776 let mut original = Extensions::new();
777 original.insert(Data(42));
778
779 let cloned = original.clone();
780
781 // Original has data
782 assert!(original.get::<Data>().is_some());
783
784 // Cloned is empty
785 assert!(cloned.is_empty());
786 assert!(cloned.get::<Data>().is_none());
787 }
788
789 #[test]
790 fn test_output_render() {
791 let output: Output<String> = Output::Render("success".into());
792 assert!(output.is_render());
793 assert!(!output.is_silent());
794 assert!(!output.is_binary());
795 }
796
797 #[test]
798 fn test_output_silent() {
799 let output: Output<String> = Output::Silent;
800 assert!(!output.is_render());
801 assert!(output.is_silent());
802 assert!(!output.is_binary());
803 }
804
805 #[test]
806 fn test_output_binary() {
807 let output: Output<String> = Output::Binary {
808 data: vec![0x25, 0x50, 0x44, 0x46],
809 filename: "report.pdf".into(),
810 };
811 assert!(!output.is_render());
812 assert!(!output.is_silent());
813 assert!(output.is_binary());
814 }
815
816 #[test]
817 fn test_run_result_handled() {
818 let result = RunResult::Handled("output".into());
819 assert!(result.is_handled());
820 assert!(!result.is_binary());
821 assert!(!result.is_silent());
822 assert_eq!(result.output(), Some("output"));
823 assert!(result.matches().is_none());
824 }
825
826 #[test]
827 fn test_run_result_silent() {
828 let result = RunResult::Silent;
829 assert!(!result.is_handled());
830 assert!(!result.is_binary());
831 assert!(result.is_silent());
832 }
833
834 #[test]
835 fn test_run_result_binary() {
836 let bytes = vec![0x25, 0x50, 0x44, 0x46];
837 let result = RunResult::Binary(bytes.clone(), "report.pdf".into());
838 assert!(!result.is_handled());
839 assert!(result.is_binary());
840 assert!(!result.is_silent());
841
842 let (data, filename) = result.binary().unwrap();
843 assert_eq!(data, &bytes);
844 assert_eq!(filename, "report.pdf");
845 }
846
847 #[test]
848 fn test_run_result_no_match() {
849 let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
850 let result = RunResult::NoMatch(matches);
851 assert!(!result.is_handled());
852 assert!(!result.is_binary());
853 assert!(result.matches().is_some());
854 }
855
856 #[test]
857 fn test_fn_handler() {
858 let handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
859 Ok(Output::Render(json!({"status": "ok"})))
860 });
861
862 let ctx = CommandContext::default();
863 let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
864
865 let result = handler.handle(&matches, &ctx);
866 assert!(result.is_ok());
867 }
868
869 #[test]
870 fn test_local_fn_handler_mutation() {
871 let mut counter = 0u32;
872
873 let mut handler = LocalFnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
874 counter += 1;
875 Ok(Output::Render(counter))
876 });
877
878 let ctx = CommandContext::default();
879 let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
880
881 let _ = handler.handle(&matches, &ctx);
882 let _ = handler.handle(&matches, &ctx);
883 let result = handler.handle(&matches, &ctx);
884
885 assert!(result.is_ok());
886 if let Ok(Output::Render(count)) = result {
887 assert_eq!(count, 3);
888 }
889 }
890}