Skip to main content

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}