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 command handlers (`&mut self`)
66
67use crate::verify::ExpectedArg;
68use clap::ArgMatches;
69use serde::Serialize;
70use std::any::{Any, TypeId};
71use std::collections::HashMap;
72use std::fmt;
73use std::rc::Rc;
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>>,
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: '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: Rc<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: Rc<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: Rc::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/// Trait for types that can be converted into a [`HandlerResult`].
377///
378/// This enables handlers to return either `Result<T, E>` directly (auto-wrapped
379/// in [`Output::Render`]) or the explicit [`HandlerResult<T>`] when fine-grained
380/// control is needed (for [`Output::Silent`] or [`Output::Binary`]).
381///
382/// # Example
383///
384/// ```rust
385/// use standout_dispatch::{HandlerResult, Output, IntoHandlerResult};
386///
387/// // Direct Result<T, E> is auto-wrapped in Output::Render
388/// fn simple() -> Result<String, anyhow::Error> {
389///     Ok("hello".to_string())
390/// }
391/// let result: HandlerResult<String> = simple().into_handler_result();
392/// assert!(matches!(result, Ok(Output::Render(_))));
393///
394/// // HandlerResult<T> passes through unchanged
395/// fn explicit() -> HandlerResult<String> {
396///     Ok(Output::Silent)
397/// }
398/// let result: HandlerResult<String> = explicit().into_handler_result();
399/// assert!(matches!(result, Ok(Output::Silent)));
400/// ```
401pub trait IntoHandlerResult<T: Serialize> {
402    /// Convert this type into a [`HandlerResult<T>`].
403    fn into_handler_result(self) -> HandlerResult<T>;
404}
405
406/// Implementation for `Result<T, E>` - auto-wraps successful values in [`Output::Render`].
407///
408/// This is the ergonomic path: handlers can return `Result<T, E>` directly
409/// and the framework wraps it appropriately.
410impl<T, E> IntoHandlerResult<T> for Result<T, E>
411where
412    T: Serialize,
413    E: Into<anyhow::Error>,
414{
415    fn into_handler_result(self) -> HandlerResult<T> {
416        self.map(Output::Render).map_err(Into::into)
417    }
418}
419
420/// Implementation for `HandlerResult<T>` - passes through unchanged.
421///
422/// This is the explicit path: handlers that need [`Output::Silent`] or
423/// [`Output::Binary`] can return `HandlerResult<T>` directly.
424impl<T: Serialize> IntoHandlerResult<T> for HandlerResult<T> {
425    fn into_handler_result(self) -> HandlerResult<T> {
426        self
427    }
428}
429
430/// Result of running the CLI dispatcher.
431///
432/// After processing arguments, the dispatcher either handles a command
433/// or falls through for manual handling.
434#[derive(Debug)]
435pub enum RunResult {
436    /// A handler processed the command; contains the rendered output
437    Handled(String),
438    /// A handler produced binary output (bytes, suggested filename)
439    Binary(Vec<u8>, String),
440    /// Silent output (handler completed but produced no output)
441    Silent,
442    /// No handler matched; contains the ArgMatches for manual handling
443    NoMatch(ArgMatches),
444}
445
446impl RunResult {
447    /// Returns true if a handler processed the command (text output).
448    pub fn is_handled(&self) -> bool {
449        matches!(self, RunResult::Handled(_))
450    }
451
452    /// Returns true if the result is binary output.
453    pub fn is_binary(&self) -> bool {
454        matches!(self, RunResult::Binary(_, _))
455    }
456
457    /// Returns true if the result is silent.
458    pub fn is_silent(&self) -> bool {
459        matches!(self, RunResult::Silent)
460    }
461
462    /// Returns the output if handled, or None otherwise.
463    pub fn output(&self) -> Option<&str> {
464        match self {
465            RunResult::Handled(s) => Some(s),
466            _ => None,
467        }
468    }
469
470    /// Returns the binary data and filename if binary, or None otherwise.
471    pub fn binary(&self) -> Option<(&[u8], &str)> {
472        match self {
473            RunResult::Binary(bytes, filename) => Some((bytes, filename)),
474            _ => None,
475        }
476    }
477
478    /// Returns the matches if unhandled, or None if handled.
479    pub fn matches(&self) -> Option<&ArgMatches> {
480        match self {
481            RunResult::NoMatch(m) => Some(m),
482            _ => None,
483        }
484    }
485}
486
487/// Trait for command handlers.
488///
489/// Handlers take `&mut self` allowing direct mutation of internal state.
490/// This is the common case for CLI applications which are single-threaded.
491///
492/// # Example
493///
494/// ```rust
495/// use standout_dispatch::{Handler, HandlerResult, Output, CommandContext};
496/// use clap::ArgMatches;
497/// use serde::Serialize;
498///
499/// struct Counter { count: u32 }
500///
501/// impl Handler for Counter {
502///     type Output = u32;
503///
504///     fn handle(&mut self, _m: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<u32> {
505///         self.count += 1;
506///         Ok(Output::Render(self.count))
507///     }
508/// }
509/// ```
510pub trait Handler {
511    /// The output type produced by this handler (must be serializable)
512    type Output: Serialize;
513
514    /// Execute the handler with the given matches and context.
515    fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext)
516        -> HandlerResult<Self::Output>;
517
518    /// Returns the arguments expected by this handler for verification.
519    ///
520    /// This is used to verify that the CLI command definition matches the handler's expectations.
521    /// Handlers generated by the `#[handler]` macro implement this automatically.
522    fn expected_args(&self) -> Vec<ExpectedArg> {
523        Vec::new()
524    }
525}
526
527/// A wrapper that implements Handler for FnMut closures.
528///
529/// The closure can return either:
530/// - `Result<T, E>` - automatically wrapped in [`Output::Render`]
531/// - `HandlerResult<T>` - passed through unchanged (for [`Output::Silent`] or [`Output::Binary`])
532///
533/// # Example
534///
535/// ```rust
536/// use standout_dispatch::{FnHandler, Handler, CommandContext, Output};
537/// use clap::ArgMatches;
538///
539/// // Returning Result<T, E> directly (auto-wrapped)
540/// let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
541///     Ok::<_, anyhow::Error>("hello".to_string())
542/// });
543///
544/// // Returning HandlerResult<T> explicitly (for Silent/Binary)
545/// let mut silent_handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
546///     Ok(Output::<()>::Silent)
547/// });
548/// ```
549pub struct FnHandler<F, T, R = HandlerResult<T>>
550where
551    T: Serialize,
552{
553    f: F,
554    _phantom: std::marker::PhantomData<fn() -> (T, R)>,
555}
556
557impl<F, T, R> FnHandler<F, T, R>
558where
559    F: FnMut(&ArgMatches, &CommandContext) -> R,
560    R: IntoHandlerResult<T>,
561    T: Serialize,
562{
563    /// Creates a new FnHandler wrapping the given FnMut closure.
564    pub fn new(f: F) -> Self {
565        Self {
566            f,
567            _phantom: std::marker::PhantomData,
568        }
569    }
570}
571
572impl<F, T, R> Handler for FnHandler<F, T, R>
573where
574    F: FnMut(&ArgMatches, &CommandContext) -> R,
575    R: IntoHandlerResult<T>,
576    T: Serialize,
577{
578    type Output = T;
579
580    fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<T> {
581        (self.f)(matches, ctx).into_handler_result()
582    }
583}
584
585/// A handler wrapper for functions that don't need [`CommandContext`].
586///
587/// This is the simpler variant of [`FnHandler`] for handlers that only need
588/// [`ArgMatches`]. The context parameter is accepted but ignored internally.
589///
590/// The closure can return either:
591/// - `Result<T, E>` - automatically wrapped in [`Output::Render`]
592/// - `HandlerResult<T>` - passed through unchanged (for [`Output::Silent`] or [`Output::Binary`])
593///
594/// # Example
595///
596/// ```rust
597/// use standout_dispatch::{SimpleFnHandler, Handler, CommandContext, Output};
598/// use clap::ArgMatches;
599///
600/// // Handler that doesn't need context - just uses ArgMatches
601/// let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
602///     Ok::<_, anyhow::Error>("Hello, world!".to_string())
603/// });
604///
605/// // Can still be used via Handler trait (context is ignored)
606/// let ctx = CommandContext::default();
607/// let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
608/// let result = handler.handle(&matches, &ctx);
609/// assert!(matches!(result, Ok(Output::Render(_))));
610/// ```
611pub struct SimpleFnHandler<F, T, R = HandlerResult<T>>
612where
613    T: Serialize,
614{
615    f: F,
616    _phantom: std::marker::PhantomData<fn() -> (T, R)>,
617}
618
619impl<F, T, R> SimpleFnHandler<F, T, R>
620where
621    F: FnMut(&ArgMatches) -> R,
622    R: IntoHandlerResult<T>,
623    T: Serialize,
624{
625    /// Creates a new SimpleFnHandler wrapping the given FnMut closure.
626    pub fn new(f: F) -> Self {
627        Self {
628            f,
629            _phantom: std::marker::PhantomData,
630        }
631    }
632}
633
634impl<F, T, R> Handler for SimpleFnHandler<F, T, R>
635where
636    F: FnMut(&ArgMatches) -> R,
637    R: IntoHandlerResult<T>,
638    T: Serialize,
639{
640    type Output = T;
641
642    fn handle(&mut self, matches: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<T> {
643        (self.f)(matches).into_handler_result()
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650    use serde_json::json;
651
652    #[test]
653    fn test_command_context_creation() {
654        let ctx = CommandContext {
655            command_path: vec!["config".into(), "get".into()],
656            app_state: Rc::new(Extensions::new()),
657            extensions: Extensions::new(),
658        };
659        assert_eq!(ctx.command_path, vec!["config", "get"]);
660    }
661
662    #[test]
663    fn test_command_context_default() {
664        let ctx = CommandContext::default();
665        assert!(ctx.command_path.is_empty());
666        assert!(ctx.extensions.is_empty());
667        assert!(ctx.app_state.is_empty());
668    }
669
670    #[test]
671    fn test_command_context_with_app_state() {
672        struct Database {
673            url: String,
674        }
675        struct Config {
676            debug: bool,
677        }
678
679        // Build app state
680        let mut app_state = Extensions::new();
681        app_state.insert(Database {
682            url: "postgres://localhost".into(),
683        });
684        app_state.insert(Config { debug: true });
685        let app_state = Rc::new(app_state);
686
687        // Create context with app state
688        let ctx = CommandContext {
689            command_path: vec!["list".into()],
690            app_state: app_state.clone(),
691            extensions: Extensions::new(),
692        };
693
694        // Retrieve app state
695        let db = ctx.app_state.get::<Database>().unwrap();
696        assert_eq!(db.url, "postgres://localhost");
697
698        let config = ctx.app_state.get::<Config>().unwrap();
699        assert!(config.debug);
700
701        // App state is shared via Rc
702        assert_eq!(Rc::strong_count(&ctx.app_state), 2);
703    }
704
705    #[test]
706    fn test_command_context_app_state_get_required() {
707        struct Present;
708
709        let mut app_state = Extensions::new();
710        app_state.insert(Present);
711
712        let ctx = CommandContext {
713            command_path: vec![],
714            app_state: Rc::new(app_state),
715            extensions: Extensions::new(),
716        };
717
718        // Success case
719        assert!(ctx.app_state.get_required::<Present>().is_ok());
720
721        // Failure case
722        #[derive(Debug)]
723        struct Missing;
724        let err = ctx.app_state.get_required::<Missing>();
725        assert!(err.is_err());
726        assert!(err.unwrap_err().to_string().contains("Extension missing"));
727    }
728
729    // Extensions tests
730    #[test]
731    fn test_extensions_insert_and_get() {
732        struct MyState {
733            value: i32,
734        }
735
736        let mut ext = Extensions::new();
737        assert!(ext.is_empty());
738
739        ext.insert(MyState { value: 42 });
740        assert!(!ext.is_empty());
741        assert_eq!(ext.len(), 1);
742
743        let state = ext.get::<MyState>().unwrap();
744        assert_eq!(state.value, 42);
745    }
746
747    #[test]
748    fn test_extensions_get_mut() {
749        struct Counter {
750            count: i32,
751        }
752
753        let mut ext = Extensions::new();
754        ext.insert(Counter { count: 0 });
755
756        if let Some(counter) = ext.get_mut::<Counter>() {
757            counter.count += 1;
758        }
759
760        assert_eq!(ext.get::<Counter>().unwrap().count, 1);
761    }
762
763    #[test]
764    fn test_extensions_multiple_types() {
765        struct TypeA(i32);
766        struct TypeB(String);
767
768        let mut ext = Extensions::new();
769        ext.insert(TypeA(1));
770        ext.insert(TypeB("hello".into()));
771
772        assert_eq!(ext.len(), 2);
773        assert_eq!(ext.get::<TypeA>().unwrap().0, 1);
774        assert_eq!(ext.get::<TypeB>().unwrap().0, "hello");
775    }
776
777    #[test]
778    fn test_extensions_replace() {
779        struct Value(i32);
780
781        let mut ext = Extensions::new();
782        ext.insert(Value(1));
783
784        let old = ext.insert(Value(2));
785        assert_eq!(old.unwrap().0, 1);
786        assert_eq!(ext.get::<Value>().unwrap().0, 2);
787    }
788
789    #[test]
790    fn test_extensions_remove() {
791        struct Value(i32);
792
793        let mut ext = Extensions::new();
794        ext.insert(Value(42));
795
796        let removed = ext.remove::<Value>();
797        assert_eq!(removed.unwrap().0, 42);
798        assert!(ext.is_empty());
799        assert!(ext.get::<Value>().is_none());
800    }
801
802    #[test]
803    fn test_extensions_contains() {
804        struct Present;
805        struct Absent;
806
807        let mut ext = Extensions::new();
808        ext.insert(Present);
809
810        assert!(ext.contains::<Present>());
811        assert!(!ext.contains::<Absent>());
812    }
813
814    #[test]
815    fn test_extensions_clear() {
816        struct A;
817        struct B;
818
819        let mut ext = Extensions::new();
820        ext.insert(A);
821        ext.insert(B);
822        assert_eq!(ext.len(), 2);
823
824        ext.clear();
825        assert!(ext.is_empty());
826    }
827
828    #[test]
829    fn test_extensions_missing_type_returns_none() {
830        struct NotInserted;
831
832        let ext = Extensions::new();
833        assert!(ext.get::<NotInserted>().is_none());
834    }
835
836    #[test]
837    fn test_extensions_get_required() {
838        #[derive(Debug)]
839        struct Config {
840            value: i32,
841        }
842
843        let mut ext = Extensions::new();
844        ext.insert(Config { value: 100 });
845
846        // Success case
847        let val = ext.get_required::<Config>();
848        assert!(val.is_ok());
849        assert_eq!(val.unwrap().value, 100);
850
851        // Failure case
852        #[derive(Debug)]
853        struct Missing;
854        let err = ext.get_required::<Missing>();
855        assert!(err.is_err());
856        assert!(err
857            .unwrap_err()
858            .to_string()
859            .contains("Extension missing: type"));
860    }
861
862    #[test]
863    fn test_extensions_get_mut_required() {
864        #[derive(Debug)]
865        struct State {
866            count: i32,
867        }
868
869        let mut ext = Extensions::new();
870        ext.insert(State { count: 0 });
871
872        // Success case
873        {
874            let val = ext.get_mut_required::<State>();
875            assert!(val.is_ok());
876            val.unwrap().count += 1;
877        }
878        assert_eq!(ext.get_required::<State>().unwrap().count, 1);
879
880        // Failure case
881        #[derive(Debug)]
882        struct Missing;
883        let err = ext.get_mut_required::<Missing>();
884        assert!(err.is_err());
885    }
886
887    #[test]
888    fn test_extensions_clone_behavior() {
889        // Verify the documented behavior that Clone drops extensions
890        struct Data(i32);
891
892        let mut original = Extensions::new();
893        original.insert(Data(42));
894
895        let cloned = original.clone();
896
897        // Original has data
898        assert!(original.get::<Data>().is_some());
899
900        // Cloned is empty
901        assert!(cloned.is_empty());
902        assert!(cloned.get::<Data>().is_none());
903    }
904
905    #[test]
906    fn test_output_render() {
907        let output: Output<String> = Output::Render("success".into());
908        assert!(output.is_render());
909        assert!(!output.is_silent());
910        assert!(!output.is_binary());
911    }
912
913    #[test]
914    fn test_output_silent() {
915        let output: Output<String> = Output::Silent;
916        assert!(!output.is_render());
917        assert!(output.is_silent());
918        assert!(!output.is_binary());
919    }
920
921    #[test]
922    fn test_output_binary() {
923        let output: Output<String> = Output::Binary {
924            data: vec![0x25, 0x50, 0x44, 0x46],
925            filename: "report.pdf".into(),
926        };
927        assert!(!output.is_render());
928        assert!(!output.is_silent());
929        assert!(output.is_binary());
930    }
931
932    #[test]
933    fn test_run_result_handled() {
934        let result = RunResult::Handled("output".into());
935        assert!(result.is_handled());
936        assert!(!result.is_binary());
937        assert!(!result.is_silent());
938        assert_eq!(result.output(), Some("output"));
939        assert!(result.matches().is_none());
940    }
941
942    #[test]
943    fn test_run_result_silent() {
944        let result = RunResult::Silent;
945        assert!(!result.is_handled());
946        assert!(!result.is_binary());
947        assert!(result.is_silent());
948    }
949
950    #[test]
951    fn test_run_result_binary() {
952        let bytes = vec![0x25, 0x50, 0x44, 0x46];
953        let result = RunResult::Binary(bytes.clone(), "report.pdf".into());
954        assert!(!result.is_handled());
955        assert!(result.is_binary());
956        assert!(!result.is_silent());
957
958        let (data, filename) = result.binary().unwrap();
959        assert_eq!(data, &bytes);
960        assert_eq!(filename, "report.pdf");
961    }
962
963    #[test]
964    fn test_run_result_no_match() {
965        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
966        let result = RunResult::NoMatch(matches);
967        assert!(!result.is_handled());
968        assert!(!result.is_binary());
969        assert!(result.matches().is_some());
970    }
971
972    #[test]
973    fn test_fn_handler() {
974        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
975            Ok(Output::Render(json!({"status": "ok"})))
976        });
977
978        let ctx = CommandContext::default();
979        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
980
981        let result = handler.handle(&matches, &ctx);
982        assert!(result.is_ok());
983    }
984
985    #[test]
986    fn test_fn_handler_mutation() {
987        let mut counter = 0u32;
988
989        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
990            counter += 1;
991            Ok(Output::Render(counter))
992        });
993
994        let ctx = CommandContext::default();
995        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
996
997        let _ = handler.handle(&matches, &ctx);
998        let _ = handler.handle(&matches, &ctx);
999        let result = handler.handle(&matches, &ctx);
1000
1001        assert!(result.is_ok());
1002        if let Ok(Output::Render(count)) = result {
1003            assert_eq!(count, 3);
1004        }
1005    }
1006
1007    // IntoHandlerResult tests
1008    #[test]
1009    fn test_into_handler_result_from_result_ok() {
1010        use super::IntoHandlerResult;
1011
1012        let result: Result<String, anyhow::Error> = Ok("hello".to_string());
1013        let handler_result = result.into_handler_result();
1014
1015        assert!(handler_result.is_ok());
1016        match handler_result.unwrap() {
1017            Output::Render(s) => assert_eq!(s, "hello"),
1018            _ => panic!("Expected Output::Render"),
1019        }
1020    }
1021
1022    #[test]
1023    fn test_into_handler_result_from_result_err() {
1024        use super::IntoHandlerResult;
1025
1026        let result: Result<String, anyhow::Error> = Err(anyhow::anyhow!("test error"));
1027        let handler_result = result.into_handler_result();
1028
1029        assert!(handler_result.is_err());
1030        assert!(handler_result
1031            .unwrap_err()
1032            .to_string()
1033            .contains("test error"));
1034    }
1035
1036    #[test]
1037    fn test_into_handler_result_passthrough_render() {
1038        use super::IntoHandlerResult;
1039
1040        let handler_result: HandlerResult<String> = Ok(Output::Render("hello".to_string()));
1041        let result = handler_result.into_handler_result();
1042
1043        assert!(result.is_ok());
1044        match result.unwrap() {
1045            Output::Render(s) => assert_eq!(s, "hello"),
1046            _ => panic!("Expected Output::Render"),
1047        }
1048    }
1049
1050    #[test]
1051    fn test_into_handler_result_passthrough_silent() {
1052        use super::IntoHandlerResult;
1053
1054        let handler_result: HandlerResult<String> = Ok(Output::Silent);
1055        let result = handler_result.into_handler_result();
1056
1057        assert!(result.is_ok());
1058        assert!(matches!(result.unwrap(), Output::Silent));
1059    }
1060
1061    #[test]
1062    fn test_into_handler_result_passthrough_binary() {
1063        use super::IntoHandlerResult;
1064
1065        let handler_result: HandlerResult<String> = Ok(Output::Binary {
1066            data: vec![1, 2, 3],
1067            filename: "test.bin".to_string(),
1068        });
1069        let result = handler_result.into_handler_result();
1070
1071        assert!(result.is_ok());
1072        match result.unwrap() {
1073            Output::Binary { data, filename } => {
1074                assert_eq!(data, vec![1, 2, 3]);
1075                assert_eq!(filename, "test.bin");
1076            }
1077            _ => panic!("Expected Output::Binary"),
1078        }
1079    }
1080
1081    #[test]
1082    fn test_fn_handler_with_auto_wrap() {
1083        // Handler that returns Result<T, E> directly (not HandlerResult)
1084        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1085            Ok::<_, anyhow::Error>("auto-wrapped".to_string())
1086        });
1087
1088        let ctx = CommandContext::default();
1089        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1090
1091        let result = handler.handle(&matches, &ctx);
1092        assert!(result.is_ok());
1093        match result.unwrap() {
1094            Output::Render(s) => assert_eq!(s, "auto-wrapped"),
1095            _ => panic!("Expected Output::Render"),
1096        }
1097    }
1098
1099    #[test]
1100    fn test_fn_handler_with_explicit_output() {
1101        // Handler that returns HandlerResult directly (for Silent/Binary)
1102        let mut handler =
1103            FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| Ok(Output::<()>::Silent));
1104
1105        let ctx = CommandContext::default();
1106        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1107
1108        let result = handler.handle(&matches, &ctx);
1109        assert!(result.is_ok());
1110        assert!(matches!(result.unwrap(), Output::Silent));
1111    }
1112
1113    #[test]
1114    fn test_fn_handler_with_custom_error_type() {
1115        // Custom error type that implements Into<anyhow::Error>
1116        #[derive(Debug)]
1117        struct CustomError(String);
1118
1119        impl std::fmt::Display for CustomError {
1120            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1121                write!(f, "CustomError: {}", self.0)
1122            }
1123        }
1124
1125        impl std::error::Error for CustomError {}
1126
1127        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1128            Err::<String, CustomError>(CustomError("oops".to_string()))
1129        });
1130
1131        let ctx = CommandContext::default();
1132        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1133
1134        let result = handler.handle(&matches, &ctx);
1135        assert!(result.is_err());
1136        assert!(result
1137            .unwrap_err()
1138            .to_string()
1139            .contains("CustomError: oops"));
1140    }
1141
1142    // SimpleFnHandler tests (no CommandContext)
1143    #[test]
1144    fn test_simple_fn_handler_basic() {
1145        use super::SimpleFnHandler;
1146
1147        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1148            Ok::<_, anyhow::Error>("no context needed".to_string())
1149        });
1150
1151        let ctx = CommandContext::default();
1152        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1153
1154        let result = handler.handle(&matches, &ctx);
1155        assert!(result.is_ok());
1156        match result.unwrap() {
1157            Output::Render(s) => assert_eq!(s, "no context needed"),
1158            _ => panic!("Expected Output::Render"),
1159        }
1160    }
1161
1162    #[test]
1163    fn test_simple_fn_handler_with_args() {
1164        use super::SimpleFnHandler;
1165
1166        let mut handler = SimpleFnHandler::new(|m: &ArgMatches| {
1167            let verbose = m.get_flag("verbose");
1168            Ok::<_, anyhow::Error>(verbose)
1169        });
1170
1171        let ctx = CommandContext::default();
1172        let matches = clap::Command::new("test")
1173            .arg(
1174                clap::Arg::new("verbose")
1175                    .short('v')
1176                    .action(clap::ArgAction::SetTrue),
1177            )
1178            .get_matches_from(vec!["test", "-v"]);
1179
1180        let result = handler.handle(&matches, &ctx);
1181        assert!(result.is_ok());
1182        match result.unwrap() {
1183            Output::Render(v) => assert!(v),
1184            _ => panic!("Expected Output::Render"),
1185        }
1186    }
1187
1188    #[test]
1189    fn test_simple_fn_handler_explicit_output() {
1190        use super::SimpleFnHandler;
1191
1192        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| Ok(Output::<()>::Silent));
1193
1194        let ctx = CommandContext::default();
1195        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1196
1197        let result = handler.handle(&matches, &ctx);
1198        assert!(result.is_ok());
1199        assert!(matches!(result.unwrap(), Output::Silent));
1200    }
1201
1202    #[test]
1203    fn test_simple_fn_handler_error() {
1204        use super::SimpleFnHandler;
1205
1206        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1207            Err::<String, _>(anyhow::anyhow!("simple error"))
1208        });
1209
1210        let ctx = CommandContext::default();
1211        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1212
1213        let result = handler.handle(&matches, &ctx);
1214        assert!(result.is_err());
1215        assert!(result.unwrap_err().to_string().contains("simple error"));
1216    }
1217
1218    #[test]
1219    fn test_simple_fn_handler_mutation() {
1220        use super::SimpleFnHandler;
1221
1222        let mut counter = 0u32;
1223        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1224            counter += 1;
1225            Ok::<_, anyhow::Error>(counter)
1226        });
1227
1228        let ctx = CommandContext::default();
1229        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1230
1231        let _ = handler.handle(&matches, &ctx);
1232        let _ = handler.handle(&matches, &ctx);
1233        let result = handler.handle(&matches, &ctx);
1234
1235        assert!(result.is_ok());
1236        match result.unwrap() {
1237            Output::Render(n) => assert_eq!(n, 3),
1238            _ => panic!("Expected Output::Render"),
1239        }
1240    }
1241}