Skip to main content

standout_dispatch/
handler.rs

1//! Command handler types.
2//!
3//! This module provides the core types for building CLI handler adapters in the
4//! dispatch pipeline.
5//!
6//! # Design Rationale
7//!
8//! Handler adapters are responsible for the CLI-to-application seam. They:
9//!
10//! - Receive parsed CLI arguments (`&ArgMatches`) and execution context
11//! - Call a CLI-free application library
12//! - Map library results into serializable CLI view data for 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::hooks::HookPhase;
68use crate::verify::ExpectedArg;
69use clap::ArgMatches;
70use serde::Serialize;
71use std::any::{Any, TypeId};
72use std::collections::HashMap;
73use std::fmt;
74use std::rc::Rc;
75use std::sync::Arc;
76
77/// Type-safe container for injecting custom state into handlers.
78///
79/// Extensions allow pre-dispatch hooks to inject state that handlers can retrieve.
80/// This enables dependency injection without modifying handler signatures.
81///
82/// # Warning: Clone Behavior
83///
84/// `Extensions` is **not** cloned when the container is cloned. Cloning an `Extensions` instance
85/// results in a new, empty map. This is because the underlying `Box<dyn Any>` values cannot
86/// be cloned generically.
87///
88/// If you need to share state across threads/clones, use `Arc<T>` inside the extension.
89///
90/// # Example
91///
92/// ```rust
93/// use standout_dispatch::{Extensions, CommandContext};
94///
95/// // Define your state types
96/// struct ApiClient { base_url: String }
97/// struct UserScope { user_id: u64 }
98///
99/// // In a pre-dispatch hook, inject state
100/// let mut ctx = CommandContext::default();
101/// ctx.extensions.insert(ApiClient { base_url: "https://api.example.com".into() });
102/// ctx.extensions.insert(UserScope { user_id: 42 });
103///
104/// // In a handler, retrieve state
105/// let api = ctx.extensions.get_required::<ApiClient>()?;
106/// println!("API base: {}", api.base_url);
107/// # Ok::<(), anyhow::Error>(())
108/// ```
109#[derive(Default)]
110pub struct Extensions {
111    map: HashMap<TypeId, Box<dyn Any>>,
112}
113
114impl Extensions {
115    /// Creates a new empty extensions container.
116    pub fn new() -> Self {
117        Self::default()
118    }
119
120    /// Inserts a value into the extensions.
121    ///
122    /// If a value of this type already exists, it is replaced and returned.
123    pub fn insert<T: 'static>(&mut self, val: T) -> Option<T> {
124        self.map
125            .insert(TypeId::of::<T>(), Box::new(val))
126            .and_then(|boxed| boxed.downcast().ok().map(|b| *b))
127    }
128
129    /// Gets a reference to a value of the specified type.
130    ///
131    /// Returns `None` if no value of this type exists.
132    pub fn get<T: 'static>(&self) -> Option<&T> {
133        self.map
134            .get(&TypeId::of::<T>())
135            .and_then(|boxed| boxed.downcast_ref())
136    }
137
138    /// Gets a mutable reference to a value of the specified type.
139    ///
140    /// Returns `None` if no value of this type exists.
141    pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
142        self.map
143            .get_mut(&TypeId::of::<T>())
144            .and_then(|boxed| boxed.downcast_mut())
145    }
146
147    /// Gets a required reference to a value of the specified type.
148    ///
149    /// Returns an error if no value of this type exists.
150    pub fn get_required<T: 'static>(&self) -> Result<&T, anyhow::Error> {
151        self.get::<T>().ok_or_else(|| {
152            anyhow::anyhow!(
153                "Extension missing: type {} not found in context",
154                std::any::type_name::<T>()
155            )
156        })
157    }
158
159    /// Gets a required mutable reference to a value of the specified type.
160    ///
161    /// Returns an error if no value of this type exists.
162    pub fn get_mut_required<T: 'static>(&mut self) -> Result<&mut T, anyhow::Error> {
163        self.get_mut::<T>().ok_or_else(|| {
164            anyhow::anyhow!(
165                "Extension missing: type {} not found in context",
166                std::any::type_name::<T>()
167            )
168        })
169    }
170
171    /// Removes a value of the specified type, returning it if it existed.
172    pub fn remove<T: 'static>(&mut self) -> Option<T> {
173        self.map
174            .remove(&TypeId::of::<T>())
175            .and_then(|boxed| boxed.downcast().ok().map(|b| *b))
176    }
177
178    /// Returns `true` if the extensions contain a value of the specified type.
179    pub fn contains<T: 'static>(&self) -> bool {
180        self.map.contains_key(&TypeId::of::<T>())
181    }
182
183    /// Returns the number of extensions stored.
184    pub fn len(&self) -> usize {
185        self.map.len()
186    }
187
188    /// Returns `true` if no extensions are stored.
189    pub fn is_empty(&self) -> bool {
190        self.map.is_empty()
191    }
192
193    /// Removes all extensions.
194    pub fn clear(&mut self) {
195        self.map.clear();
196    }
197}
198
199impl fmt::Debug for Extensions {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.debug_struct("Extensions")
202            .field("len", &self.map.len())
203            .finish_non_exhaustive()
204    }
205}
206
207impl Clone for Extensions {
208    fn clone(&self) -> Self {
209        // Extensions cannot be cloned because Box<dyn Any> isn't Clone.
210        // Return empty extensions on clone - this is a limitation but
211        // matches the behavior of http::Extensions.
212        Self::new()
213    }
214}
215
216/// Context passed to command handlers.
217///
218/// Provides information about the execution environment plus two mechanisms
219/// for state injection:
220///
221/// - **`app_state`**: Immutable, app-lifetime state (Database, Config, API clients)
222/// - **`extensions`**: Mutable, per-request state (UserScope, RequestId)
223///
224/// Note that output format is deliberately not included here - format decisions
225/// are made by the render handler, not by logic handlers.
226///
227/// # App State (Immutable, Shared)
228///
229/// App state is configured at build time and shared across all dispatches:
230///
231/// ```rust,ignore
232/// use standout::cli::App;
233///
234/// struct Database { /* ... */ }
235/// struct Config { api_url: String }
236///
237/// App::builder()
238///     .app_state(Database::connect()?)
239///     .app_state(Config { api_url: "https://api.example.com".into() })
240///     .command("list", list_handler, "{{ items }}")
241///     .build()?
242/// ```
243///
244/// Handlers retrieve app state immutably:
245///
246/// ```rust,ignore
247/// fn list_handler(matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Vec<Item>> {
248///     let db = ctx.app_state.get_required::<Database>()?;
249///     let config = ctx.app_state.get_required::<Config>()?;
250///     Ok(Output::Render(db.list_items(&config.api_url)?))
251/// }
252/// ```
253///
254/// ## Shared Mutable State
255///
256/// Since `app_state` is shared via `Arc`, it is immutable by default. To share mutable state
257/// (like counters or caches), use interior mutability primitives like `RwLock`, `Mutex`, or atomic types:
258///
259/// ```rust,ignore
260/// use std::sync::atomic::AtomicUsize;
261///
262/// struct Metrics { request_count: AtomicUsize }
263///
264/// // Builder
265/// App::builder().app_state(Metrics { request_count: AtomicUsize::new(0) });
266///
267/// // Handler
268/// let metrics = ctx.app_state.get_required::<Metrics>()?;
269/// metrics.request_count.fetch_add(1, Ordering::Relaxed);
270/// ```
271///
272/// # Extensions (Mutable, Per-Request)
273///
274/// Pre-dispatch hooks inject per-request state into `extensions`:
275///
276/// ```rust
277/// use standout_dispatch::{Hooks, HookError, CommandContext};
278///
279/// struct UserScope { user_id: String }
280///
281/// let hooks = Hooks::new()
282///     .pre_dispatch(|matches, ctx| {
283///         let user_id = matches.get_one::<String>("user").unwrap();
284///         ctx.extensions.insert(UserScope { user_id: user_id.clone() });
285///         Ok(())
286///     });
287///
288/// // In handler:
289/// fn my_handler(matches: &clap::ArgMatches, ctx: &CommandContext) -> anyhow::Result<()> {
290///     let scope = ctx.extensions.get_required::<UserScope>()?;
291///     // use scope.user_id...
292///     Ok(())
293/// }
294/// ```
295#[derive(Debug)]
296pub struct CommandContext {
297    /// The command path being executed (e.g., ["config", "get"])
298    pub command_path: Vec<String>,
299
300    /// Immutable app-level state shared across all dispatches.
301    ///
302    /// Configured via `AppBuilder::app_state()`. Contains long-lived resources
303    /// like database connections, configuration, and API clients.
304    ///
305    /// Use `get::<T>()` or `get_required::<T>()` to retrieve values.
306    pub app_state: Rc<Extensions>,
307
308    /// Mutable per-request state container.
309    ///
310    /// Pre-dispatch hooks can insert values that handlers retrieve.
311    /// Each dispatch gets a fresh Extensions instance.
312    pub extensions: Extensions,
313}
314
315impl CommandContext {
316    /// Creates a new CommandContext with the given path and shared app state.
317    ///
318    /// This is more efficient than `Default::default()` when you already have app_state.
319    pub fn new(command_path: Vec<String>, app_state: Rc<Extensions>) -> Self {
320        Self {
321            command_path,
322            app_state,
323            extensions: Extensions::new(),
324        }
325    }
326}
327
328impl Default for CommandContext {
329    fn default() -> Self {
330        Self {
331            command_path: Vec::new(),
332            app_state: Rc::new(Extensions::new()),
333            extensions: Extensions::new(),
334        }
335    }
336}
337
338/// What a handler produces.
339///
340/// This enum represents the different types of output a command handler can produce.
341#[derive(Debug)]
342pub enum Output<T: Serialize> {
343    /// Data to render with a template or serialize to JSON/YAML/etc.
344    Render(T),
345    /// Silent exit (no output produced)
346    Silent,
347    /// Binary output for file exports
348    Binary {
349        /// The binary data
350        data: Vec<u8>,
351        /// Suggested filename for the output
352        filename: String,
353    },
354}
355
356impl<T: Serialize> Output<T> {
357    /// Returns true if this is a render result.
358    pub fn is_render(&self) -> bool {
359        matches!(self, Output::Render(_))
360    }
361
362    /// Returns true if this is a silent result.
363    pub fn is_silent(&self) -> bool {
364        matches!(self, Output::Silent)
365    }
366
367    /// Returns true if this is a binary result.
368    pub fn is_binary(&self) -> bool {
369        matches!(self, Output::Binary { .. })
370    }
371}
372
373/// The result type for command handlers.
374///
375/// Enables use of the `?` operator for error propagation.
376pub type HandlerResult<T> = Result<Output<T>, anyhow::Error>;
377
378/// Trait for types that can be converted into a [`HandlerResult`].
379///
380/// This enables handlers to return either `Result<T, E>` directly (auto-wrapped
381/// in [`Output::Render`]) or the explicit [`HandlerResult<T>`] when fine-grained
382/// control is needed (for [`Output::Silent`] or [`Output::Binary`]).
383///
384/// # Example
385///
386/// ```rust
387/// use standout_dispatch::{HandlerResult, Output, IntoHandlerResult};
388///
389/// // Direct Result<T, E> is auto-wrapped in Output::Render
390/// fn simple() -> Result<String, anyhow::Error> {
391///     Ok("hello".to_string())
392/// }
393/// let result: HandlerResult<String> = simple().into_handler_result();
394/// assert!(matches!(result, Ok(Output::Render(_))));
395///
396/// // HandlerResult<T> passes through unchanged
397/// fn explicit() -> HandlerResult<String> {
398///     Ok(Output::Silent)
399/// }
400/// let result: HandlerResult<String> = explicit().into_handler_result();
401/// assert!(matches!(result, Ok(Output::Silent)));
402/// ```
403pub trait IntoHandlerResult<T: Serialize> {
404    /// Convert this type into a [`HandlerResult<T>`].
405    fn into_handler_result(self) -> HandlerResult<T>;
406}
407
408/// Implementation for `Result<T, E>` - auto-wraps successful values in [`Output::Render`].
409///
410/// This is the ergonomic path: handlers can return `Result<T, E>` directly
411/// and the framework wraps it appropriately.
412impl<T, E> IntoHandlerResult<T> for Result<T, E>
413where
414    T: Serialize,
415    E: Into<anyhow::Error>,
416{
417    fn into_handler_result(self) -> HandlerResult<T> {
418        self.map(Output::Render).map_err(Into::into)
419    }
420}
421
422/// Implementation for `HandlerResult<T>` - passes through unchanged.
423///
424/// This is the explicit path: handlers that need [`Output::Silent`] or
425/// [`Output::Binary`] can return `HandlerResult<T>` directly.
426impl<T: Serialize> IntoHandlerResult<T> for HandlerResult<T> {
427    fn into_handler_result(self) -> HandlerResult<T> {
428        self
429    }
430}
431
432/// Process exit status selected by Standout's execution policy.
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
434pub struct ExitStatus(u8);
435
436impl ExitStatus {
437    /// Successful command, help, or version display.
438    pub const SUCCESS: Self = Self(0);
439    /// Runtime or final-write failure.
440    pub const FAILURE: Self = Self(1);
441    /// Clap command-line usage error.
442    pub const USAGE_ERROR: Self = Self(2);
443
444    /// Returns the numeric status reported to the operating system.
445    pub const fn code(self) -> u8 {
446        self.0
447    }
448}
449
450/// Error returned when an external failure attempts to declare success.
451#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
452#[error("an external failure status must be nonzero")]
453pub struct InvalidExternalStatus;
454
455/// An application-declared failure from an authoritative external operation.
456///
457/// Use this narrow escape hatch when a command delegates to another executable
458/// and must preserve that executable's exact nonzero status and stderr payload.
459/// Returning ordinary handler or hook errors continues to use Standout's normal
460/// runtime status (`1`); this type does not configure general error mapping.
461///
462/// `ExternalFailure` can flow through a handler's existing [`HandlerResult`]
463/// error seam via `?`/`Into<anyhow::Error>`. For pre-dispatch, wrap it with
464/// [`HookError::pre_dispatch_external`](crate::HookError::pre_dispatch_external).
465#[derive(Debug, Clone)]
466pub struct ExternalFailure {
467    status: ExitStatus,
468    diagnostic: String,
469    source: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
470}
471
472impl ExternalFailure {
473    /// Declares an external failure with an exact nonzero process status and
474    /// verbatim stderr payload.
475    pub fn new(status: u8, diagnostic: impl Into<String>) -> Result<Self, InvalidExternalStatus> {
476        if status == 0 {
477            return Err(InvalidExternalStatus);
478        }
479        Ok(Self {
480            status: ExitStatus(status),
481            diagnostic: diagnostic.into(),
482            source: None,
483        })
484    }
485
486    /// Returns the exact status declared by the application.
487    pub const fn exit_status(&self) -> ExitStatus {
488        self.status
489    }
490
491    /// Returns the stderr payload without adding or removing any text.
492    pub fn diagnostic(&self) -> &str {
493        &self.diagnostic
494    }
495
496    /// Attaches the error that caused the external operation to fail.
497    pub fn with_source<E>(mut self, source: E) -> Self
498    where
499        E: std::error::Error + Send + Sync + 'static,
500    {
501        self.source = Some(Arc::new(source));
502        self
503    }
504}
505
506impl fmt::Display for ExternalFailure {
507    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
508        f.write_str(self.diagnostic())
509    }
510}
511
512impl std::error::Error for ExternalFailure {
513    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
514        self.source
515            .as_deref()
516            .map(|source| source as &(dyn std::error::Error + 'static))
517    }
518}
519
520/// Successful text outcome carried by [`RunResult::Handled`].
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
522#[non_exhaustive]
523pub enum SuccessKind {
524    /// A registered command completed successfully.
525    Command,
526    /// Clap requested a help display.
527    ClapHelp,
528    /// Clap requested a version display.
529    ClapVersion,
530}
531
532/// The final payload whose write failed.
533#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
534#[non_exhaustive]
535pub enum OutputKind {
536    /// Rendered text output.
537    Text,
538    /// Binary output.
539    Binary,
540}
541
542/// Typed origin for an unsuccessful framework run.
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
544#[non_exhaustive]
545pub enum RunErrorKind {
546    /// Clap rejected the command line.
547    ClapUsage,
548    /// The application handler returned an error.
549    Handler,
550    /// A hook failed in the identified phase. Pipe failures are post-output hooks.
551    Hook(HookPhase),
552    /// Handler data serialization or presentation rendering failed.
553    Render,
554    /// A framework-owned file/stdout write failed.
555    FinalWrite(OutputKind),
556    /// The application declared a failure from an authoritative external operation.
557    External,
558}
559
560/// Metadata-bearing successful text compatible with string-oriented access.
561#[derive(Debug, Clone)]
562pub struct RunOutput {
563    text: String,
564    kind: SuccessKind,
565}
566
567impl RunOutput {
568    /// Creates a successful command output.
569    pub fn command(text: impl Into<String>) -> Self {
570        Self {
571            text: text.into(),
572            kind: SuccessKind::Command,
573        }
574    }
575
576    /// Creates a Clap help display.
577    pub fn clap_help(text: impl Into<String>) -> Self {
578        Self {
579            text: text.into(),
580            kind: SuccessKind::ClapHelp,
581        }
582    }
583
584    /// Creates a Clap version display.
585    pub fn clap_version(text: impl Into<String>) -> Self {
586        Self {
587            text: text.into(),
588            kind: SuccessKind::ClapVersion,
589        }
590    }
591
592    /// Returns the captured text.
593    pub fn as_str(&self) -> &str {
594        &self.text
595    }
596
597    /// Returns the typed success origin.
598    pub const fn kind(&self) -> SuccessKind {
599        self.kind
600    }
601
602    /// Consumes the wrapper and returns the captured text.
603    pub fn into_string(self) -> String {
604        self.text
605    }
606}
607
608impl std::ops::Deref for RunOutput {
609    type Target = str;
610    fn deref(&self) -> &Self::Target {
611        self.as_str()
612    }
613}
614
615impl AsRef<str> for RunOutput {
616    fn as_ref(&self) -> &str {
617        self.as_str()
618    }
619}
620
621impl fmt::Display for RunOutput {
622    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623        f.write_str(self.as_str())
624    }
625}
626
627impl PartialEq<str> for RunOutput {
628    fn eq(&self, other: &str) -> bool {
629        self.as_str() == other
630    }
631}
632
633impl PartialEq<&str> for RunOutput {
634    fn eq(&self, other: &&str) -> bool {
635        self.as_str() == *other
636    }
637}
638
639impl PartialEq<String> for RunOutput {
640    fn eq(&self, other: &String) -> bool {
641        self.as_str() == other
642    }
643}
644
645impl From<String> for RunOutput {
646    fn from(text: String) -> Self {
647        Self::command(text)
648    }
649}
650
651impl From<&str> for RunOutput {
652    fn from(text: &str) -> Self {
653        Self::command(text)
654    }
655}
656
657impl From<RunOutput> for String {
658    fn from(output: RunOutput) -> Self {
659        output.into_string()
660    }
661}
662
663/// Metadata-bearing failure compatible with string-oriented access.
664#[derive(Debug, Clone)]
665pub struct RunError {
666    message: String,
667    kind: RunErrorKind,
668    status: ExitStatus,
669    source: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
670}
671
672impl RunError {
673    /// Creates a failure with its framework origin.
674    ///
675    /// # Panics
676    ///
677    /// Panics for [`RunErrorKind::External`]. External outcomes must be
678    /// constructed through [`ExternalFailure`] so their nonzero declared
679    /// status and verbatim diagnostic cannot become inconsistent.
680    pub fn new(message: impl Into<String>, kind: RunErrorKind) -> Self {
681        assert!(
682            kind != RunErrorKind::External,
683            "external run errors must be constructed from ExternalFailure"
684        );
685        let status = match kind {
686            RunErrorKind::ClapUsage => ExitStatus::USAGE_ERROR,
687            _ => ExitStatus::FAILURE,
688        };
689        Self {
690            message: message.into(),
691            kind,
692            status,
693            source: None,
694        }
695    }
696
697    /// Attaches the error that caused this captured failure.
698    pub fn with_source<E>(mut self, source: E) -> Self
699    where
700        E: std::error::Error + Send + Sync + 'static,
701    {
702        self.source = Some(Arc::new(source));
703        self
704    }
705
706    /// Returns the existing human-readable diagnostic.
707    pub fn as_str(&self) -> &str {
708        &self.message
709    }
710
711    /// Returns the typed failure origin.
712    pub const fn kind(&self) -> RunErrorKind {
713        self.kind
714    }
715
716    /// Returns the shell status selected for this failure.
717    pub const fn exit_status(&self) -> ExitStatus {
718        self.status
719    }
720
721    /// Consumes the wrapper and returns the diagnostic text.
722    pub fn into_string(self) -> String {
723        self.message
724    }
725}
726
727impl std::ops::Deref for RunError {
728    type Target = str;
729    fn deref(&self) -> &Self::Target {
730        self.as_str()
731    }
732}
733
734impl AsRef<str> for RunError {
735    fn as_ref(&self) -> &str {
736        self.as_str()
737    }
738}
739
740impl fmt::Display for RunError {
741    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742        f.write_str(self.as_str())
743    }
744}
745
746impl std::error::Error for RunError {
747    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
748        self.source
749            .as_deref()
750            .map(|source| source as &(dyn std::error::Error + 'static))
751    }
752}
753
754impl From<ExternalFailure> for RunError {
755    fn from(failure: ExternalFailure) -> Self {
756        Self {
757            message: failure.diagnostic,
758            kind: RunErrorKind::External,
759            status: failure.status,
760            source: failure.source,
761        }
762    }
763}
764
765impl From<String> for RunError {
766    fn from(message: String) -> Self {
767        Self::new(message, RunErrorKind::Handler)
768    }
769}
770
771impl From<&str> for RunError {
772    fn from(message: &str) -> Self {
773        Self::new(message, RunErrorKind::Handler)
774    }
775}
776
777impl From<RunError> for String {
778    fn from(error: RunError) -> Self {
779        error.into_string()
780    }
781}
782
783/// Result of running the CLI dispatcher.
784///
785/// After processing arguments, the dispatcher either handles a command,
786/// surfaces an error, or falls through for manual handling.
787///
788/// Marked `#[non_exhaustive]` so future variants can be added without
789/// breaking exhaustive matchers.
790#[derive(Debug)]
791#[non_exhaustive]
792pub enum RunResult {
793    /// A handler processed the command successfully; contains the rendered output
794    Handled(RunOutput),
795    /// A handler produced binary output (bytes, suggested filename)
796    Binary(Vec<u8>, String),
797    /// Silent output (handler completed but produced no output)
798    Silent,
799    /// A handler, hook, or output step failed; contains the formatted error message.
800    /// Consumers should write this to stderr and exit non-zero.
801    Error(RunError),
802    /// No handler matched; contains the ArgMatches for manual handling
803    NoMatch(ArgMatches),
804}
805
806impl RunResult {
807    /// Returns true if a handler processed the command successfully (text output).
808    pub fn is_handled(&self) -> bool {
809        matches!(self, RunResult::Handled(_))
810    }
811
812    /// Returns true if the result is binary output.
813    pub fn is_binary(&self) -> bool {
814        matches!(self, RunResult::Binary(_, _))
815    }
816
817    /// Returns true if the result is silent.
818    pub fn is_silent(&self) -> bool {
819        matches!(self, RunResult::Silent)
820    }
821
822    /// Returns true if the result is an error.
823    pub fn is_error(&self) -> bool {
824        matches!(self, RunResult::Error(_))
825    }
826
827    /// Returns the output if handled, or None otherwise.
828    pub fn output(&self) -> Option<&str> {
829        match self {
830            RunResult::Handled(s) => Some(s),
831            _ => None,
832        }
833    }
834
835    /// Returns the error message if this is an error, or None otherwise.
836    pub fn error(&self) -> Option<&str> {
837        match self {
838            RunResult::Error(s) => Some(s),
839            _ => None,
840        }
841    }
842
843    /// Returns the typed success origin for captured text.
844    pub fn success_kind(&self) -> Option<SuccessKind> {
845        match self {
846            RunResult::Handled(output) => Some(output.kind()),
847            RunResult::Binary(_, _) | RunResult::Silent => Some(SuccessKind::Command),
848            _ => None,
849        }
850    }
851
852    /// Returns the typed error origin, if this run failed.
853    pub fn error_kind(&self) -> Option<RunErrorKind> {
854        match self {
855            RunResult::Error(error) => Some(error.kind()),
856            _ => None,
857        }
858    }
859
860    /// Returns the completed run's shell status.
861    ///
862    /// `NoMatch` returns `None`: it is a fallback handoff, not a completed
863    /// framework execution and is deliberately not treated as a usage error.
864    pub fn exit_status(&self) -> Option<ExitStatus> {
865        match self {
866            RunResult::Handled(_) | RunResult::Binary(_, _) | RunResult::Silent => {
867                Some(ExitStatus::SUCCESS)
868            }
869            RunResult::Error(error) => Some(error.exit_status()),
870            RunResult::NoMatch(_) => None,
871        }
872    }
873
874    /// Returns the binary data and filename if binary, or None otherwise.
875    pub fn binary(&self) -> Option<(&[u8], &str)> {
876        match self {
877            RunResult::Binary(bytes, filename) => Some((bytes, filename)),
878            _ => None,
879        }
880    }
881
882    /// Returns the matches if unhandled, or None if handled.
883    pub fn matches(&self) -> Option<&ArgMatches> {
884        match self {
885            RunResult::NoMatch(m) => Some(m),
886            _ => None,
887        }
888    }
889}
890
891/// Trait for command handlers.
892///
893/// Handlers take `&mut self` allowing direct mutation of internal state.
894/// This is the common case for CLI applications which are single-threaded.
895///
896/// # Example
897///
898/// ```rust
899/// use standout_dispatch::{Handler, HandlerResult, Output, CommandContext};
900/// use clap::ArgMatches;
901/// use serde::Serialize;
902///
903/// struct Counter { count: u32 }
904///
905/// impl Handler for Counter {
906///     type Output = u32;
907///
908///     fn handle(&mut self, _m: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<u32> {
909///         self.count += 1;
910///         Ok(Output::Render(self.count))
911///     }
912/// }
913/// ```
914pub trait Handler {
915    /// The output type produced by this handler (must be serializable)
916    type Output: Serialize;
917
918    /// Execute the handler with the given matches and context.
919    fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext)
920        -> HandlerResult<Self::Output>;
921
922    /// Returns the arguments expected by this handler for verification.
923    ///
924    /// This is used to verify that the CLI command definition matches the handler's expectations.
925    /// Handlers generated by the `#[handler]` macro implement this automatically.
926    fn expected_args(&self) -> Vec<ExpectedArg> {
927        Vec::new()
928    }
929}
930
931/// A wrapper that implements Handler for FnMut closures.
932///
933/// The closure can return either:
934/// - `Result<T, E>` - automatically wrapped in [`Output::Render`]
935/// - `HandlerResult<T>` - passed through unchanged (for [`Output::Silent`] or [`Output::Binary`])
936///
937/// # Example
938///
939/// ```rust
940/// use standout_dispatch::{FnHandler, Handler, CommandContext, Output};
941/// use clap::ArgMatches;
942///
943/// // Returning Result<T, E> directly (auto-wrapped)
944/// let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
945///     Ok::<_, anyhow::Error>("hello".to_string())
946/// });
947///
948/// // Returning HandlerResult<T> explicitly (for Silent/Binary)
949/// let mut silent_handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
950///     Ok(Output::<()>::Silent)
951/// });
952/// ```
953pub struct FnHandler<F, T, R = HandlerResult<T>>
954where
955    T: Serialize,
956{
957    f: F,
958    _phantom: std::marker::PhantomData<fn() -> (T, R)>,
959}
960
961impl<F, T, R> FnHandler<F, T, R>
962where
963    F: FnMut(&ArgMatches, &CommandContext) -> R,
964    R: IntoHandlerResult<T>,
965    T: Serialize,
966{
967    /// Creates a new FnHandler wrapping the given FnMut closure.
968    pub fn new(f: F) -> Self {
969        Self {
970            f,
971            _phantom: std::marker::PhantomData,
972        }
973    }
974}
975
976impl<F, T, R> Handler for FnHandler<F, T, R>
977where
978    F: FnMut(&ArgMatches, &CommandContext) -> R,
979    R: IntoHandlerResult<T>,
980    T: Serialize,
981{
982    type Output = T;
983
984    fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<T> {
985        (self.f)(matches, ctx).into_handler_result()
986    }
987}
988
989/// A handler wrapper for functions that don't need [`CommandContext`].
990///
991/// This is the simpler variant of [`FnHandler`] for handlers that only need
992/// [`ArgMatches`]. The context parameter is accepted but ignored internally.
993///
994/// The closure can return either:
995/// - `Result<T, E>` - automatically wrapped in [`Output::Render`]
996/// - `HandlerResult<T>` - passed through unchanged (for [`Output::Silent`] or [`Output::Binary`])
997///
998/// # Example
999///
1000/// ```rust
1001/// use standout_dispatch::{SimpleFnHandler, Handler, CommandContext, Output};
1002/// use clap::ArgMatches;
1003///
1004/// // Handler that doesn't need context - just uses ArgMatches
1005/// let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1006///     Ok::<_, anyhow::Error>("Hello, world!".to_string())
1007/// });
1008///
1009/// // Can still be used via Handler trait (context is ignored)
1010/// let ctx = CommandContext::default();
1011/// let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1012/// let result = handler.handle(&matches, &ctx);
1013/// assert!(matches!(result, Ok(Output::Render(_))));
1014/// ```
1015pub struct SimpleFnHandler<F, T, R = HandlerResult<T>>
1016where
1017    T: Serialize,
1018{
1019    f: F,
1020    _phantom: std::marker::PhantomData<fn() -> (T, R)>,
1021}
1022
1023impl<F, T, R> SimpleFnHandler<F, T, R>
1024where
1025    F: FnMut(&ArgMatches) -> R,
1026    R: IntoHandlerResult<T>,
1027    T: Serialize,
1028{
1029    /// Creates a new SimpleFnHandler wrapping the given FnMut closure.
1030    pub fn new(f: F) -> Self {
1031        Self {
1032            f,
1033            _phantom: std::marker::PhantomData,
1034        }
1035    }
1036}
1037
1038impl<F, T, R> Handler for SimpleFnHandler<F, T, R>
1039where
1040    F: FnMut(&ArgMatches) -> R,
1041    R: IntoHandlerResult<T>,
1042    T: Serialize,
1043{
1044    type Output = T;
1045
1046    fn handle(&mut self, matches: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<T> {
1047        (self.f)(matches).into_handler_result()
1048    }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053    use super::*;
1054    use serde_json::json;
1055
1056    #[test]
1057    fn test_command_context_creation() {
1058        let ctx = CommandContext {
1059            command_path: vec!["config".into(), "get".into()],
1060            app_state: Rc::new(Extensions::new()),
1061            extensions: Extensions::new(),
1062        };
1063        assert_eq!(ctx.command_path, vec!["config", "get"]);
1064    }
1065
1066    #[test]
1067    fn external_failure_rejects_success_and_preserves_metadata() {
1068        assert_eq!(
1069            ExternalFailure::new(0, "not a failure").unwrap_err(),
1070            InvalidExternalStatus
1071        );
1072
1073        let failure = ExternalFailure::new(128, "fatal: repository missing\n")
1074            .unwrap()
1075            .with_source(std::io::Error::other("git failed"));
1076        assert_eq!(failure.exit_status().code(), 128);
1077        assert_eq!(failure.diagnostic(), "fatal: repository missing\n");
1078        assert_eq!(
1079            std::error::Error::source(&failure).unwrap().to_string(),
1080            "git failed"
1081        );
1082
1083        let captured = RunError::from(failure);
1084        assert_eq!(captured.kind(), RunErrorKind::External);
1085        assert_eq!(captured.exit_status().code(), 128);
1086        assert_eq!(captured.as_str(), "fatal: repository missing\n");
1087        assert_eq!(
1088            std::error::Error::source(&captured).unwrap().to_string(),
1089            "git failed"
1090        );
1091    }
1092
1093    #[test]
1094    #[should_panic(expected = "external run errors must be constructed from ExternalFailure")]
1095    fn run_error_new_rejects_external_kind() {
1096        let _ = RunError::new("inconsistent", RunErrorKind::External);
1097    }
1098
1099    #[test]
1100    fn test_command_context_default() {
1101        let ctx = CommandContext::default();
1102        assert!(ctx.command_path.is_empty());
1103        assert!(ctx.extensions.is_empty());
1104        assert!(ctx.app_state.is_empty());
1105    }
1106
1107    #[test]
1108    fn test_command_context_with_app_state() {
1109        struct Database {
1110            url: String,
1111        }
1112        struct Config {
1113            debug: bool,
1114        }
1115
1116        // Build app state
1117        let mut app_state = Extensions::new();
1118        app_state.insert(Database {
1119            url: "postgres://localhost".into(),
1120        });
1121        app_state.insert(Config { debug: true });
1122        let app_state = Rc::new(app_state);
1123
1124        // Create context with app state
1125        let ctx = CommandContext {
1126            command_path: vec!["list".into()],
1127            app_state: app_state.clone(),
1128            extensions: Extensions::new(),
1129        };
1130
1131        // Retrieve app state
1132        let db = ctx.app_state.get::<Database>().unwrap();
1133        assert_eq!(db.url, "postgres://localhost");
1134
1135        let config = ctx.app_state.get::<Config>().unwrap();
1136        assert!(config.debug);
1137
1138        // App state is shared via Rc
1139        assert_eq!(Rc::strong_count(&ctx.app_state), 2);
1140    }
1141
1142    #[test]
1143    fn test_command_context_app_state_get_required() {
1144        struct Present;
1145
1146        let mut app_state = Extensions::new();
1147        app_state.insert(Present);
1148
1149        let ctx = CommandContext {
1150            command_path: vec![],
1151            app_state: Rc::new(app_state),
1152            extensions: Extensions::new(),
1153        };
1154
1155        // Success case
1156        assert!(ctx.app_state.get_required::<Present>().is_ok());
1157
1158        // Failure case
1159        #[derive(Debug)]
1160        struct Missing;
1161        let err = ctx.app_state.get_required::<Missing>();
1162        assert!(err.is_err());
1163        assert!(err.unwrap_err().to_string().contains("Extension missing"));
1164    }
1165
1166    // Extensions tests
1167    #[test]
1168    fn test_extensions_insert_and_get() {
1169        struct MyState {
1170            value: i32,
1171        }
1172
1173        let mut ext = Extensions::new();
1174        assert!(ext.is_empty());
1175
1176        ext.insert(MyState { value: 42 });
1177        assert!(!ext.is_empty());
1178        assert_eq!(ext.len(), 1);
1179
1180        let state = ext.get::<MyState>().unwrap();
1181        assert_eq!(state.value, 42);
1182    }
1183
1184    #[test]
1185    fn test_extensions_get_mut() {
1186        struct Counter {
1187            count: i32,
1188        }
1189
1190        let mut ext = Extensions::new();
1191        ext.insert(Counter { count: 0 });
1192
1193        if let Some(counter) = ext.get_mut::<Counter>() {
1194            counter.count += 1;
1195        }
1196
1197        assert_eq!(ext.get::<Counter>().unwrap().count, 1);
1198    }
1199
1200    #[test]
1201    fn test_extensions_multiple_types() {
1202        struct TypeA(i32);
1203        struct TypeB(String);
1204
1205        let mut ext = Extensions::new();
1206        ext.insert(TypeA(1));
1207        ext.insert(TypeB("hello".into()));
1208
1209        assert_eq!(ext.len(), 2);
1210        assert_eq!(ext.get::<TypeA>().unwrap().0, 1);
1211        assert_eq!(ext.get::<TypeB>().unwrap().0, "hello");
1212    }
1213
1214    #[test]
1215    fn test_extensions_replace() {
1216        struct Value(i32);
1217
1218        let mut ext = Extensions::new();
1219        ext.insert(Value(1));
1220
1221        let old = ext.insert(Value(2));
1222        assert_eq!(old.unwrap().0, 1);
1223        assert_eq!(ext.get::<Value>().unwrap().0, 2);
1224    }
1225
1226    #[test]
1227    fn test_extensions_remove() {
1228        struct Value(i32);
1229
1230        let mut ext = Extensions::new();
1231        ext.insert(Value(42));
1232
1233        let removed = ext.remove::<Value>();
1234        assert_eq!(removed.unwrap().0, 42);
1235        assert!(ext.is_empty());
1236        assert!(ext.get::<Value>().is_none());
1237    }
1238
1239    #[test]
1240    fn test_extensions_contains() {
1241        struct Present;
1242        struct Absent;
1243
1244        let mut ext = Extensions::new();
1245        ext.insert(Present);
1246
1247        assert!(ext.contains::<Present>());
1248        assert!(!ext.contains::<Absent>());
1249    }
1250
1251    #[test]
1252    fn test_extensions_clear() {
1253        struct A;
1254        struct B;
1255
1256        let mut ext = Extensions::new();
1257        ext.insert(A);
1258        ext.insert(B);
1259        assert_eq!(ext.len(), 2);
1260
1261        ext.clear();
1262        assert!(ext.is_empty());
1263    }
1264
1265    #[test]
1266    fn test_extensions_missing_type_returns_none() {
1267        struct NotInserted;
1268
1269        let ext = Extensions::new();
1270        assert!(ext.get::<NotInserted>().is_none());
1271    }
1272
1273    #[test]
1274    fn test_extensions_get_required() {
1275        #[derive(Debug)]
1276        struct Config {
1277            value: i32,
1278        }
1279
1280        let mut ext = Extensions::new();
1281        ext.insert(Config { value: 100 });
1282
1283        // Success case
1284        let val = ext.get_required::<Config>();
1285        assert!(val.is_ok());
1286        assert_eq!(val.unwrap().value, 100);
1287
1288        // Failure case
1289        #[derive(Debug)]
1290        struct Missing;
1291        let err = ext.get_required::<Missing>();
1292        assert!(err.is_err());
1293        assert!(err
1294            .unwrap_err()
1295            .to_string()
1296            .contains("Extension missing: type"));
1297    }
1298
1299    #[test]
1300    fn test_extensions_get_mut_required() {
1301        #[derive(Debug)]
1302        struct State {
1303            count: i32,
1304        }
1305
1306        let mut ext = Extensions::new();
1307        ext.insert(State { count: 0 });
1308
1309        // Success case
1310        {
1311            let val = ext.get_mut_required::<State>();
1312            assert!(val.is_ok());
1313            val.unwrap().count += 1;
1314        }
1315        assert_eq!(ext.get_required::<State>().unwrap().count, 1);
1316
1317        // Failure case
1318        #[derive(Debug)]
1319        struct Missing;
1320        let err = ext.get_mut_required::<Missing>();
1321        assert!(err.is_err());
1322    }
1323
1324    #[test]
1325    fn test_extensions_clone_behavior() {
1326        // Verify the documented behavior that Clone drops extensions
1327        struct Data(#[allow(dead_code)] i32);
1328
1329        let mut original = Extensions::new();
1330        original.insert(Data(42));
1331
1332        let cloned = original.clone();
1333
1334        // Original has data
1335        assert!(original.get::<Data>().is_some());
1336
1337        // Cloned is empty
1338        assert!(cloned.is_empty());
1339        assert!(cloned.get::<Data>().is_none());
1340    }
1341
1342    #[test]
1343    fn test_output_render() {
1344        let output: Output<String> = Output::Render("success".into());
1345        assert!(output.is_render());
1346        assert!(!output.is_silent());
1347        assert!(!output.is_binary());
1348    }
1349
1350    #[test]
1351    fn test_output_silent() {
1352        let output: Output<String> = Output::Silent;
1353        assert!(!output.is_render());
1354        assert!(output.is_silent());
1355        assert!(!output.is_binary());
1356    }
1357
1358    #[test]
1359    fn test_output_binary() {
1360        let output: Output<String> = Output::Binary {
1361            data: vec![0x25, 0x50, 0x44, 0x46],
1362            filename: "report.pdf".into(),
1363        };
1364        assert!(!output.is_render());
1365        assert!(!output.is_silent());
1366        assert!(output.is_binary());
1367    }
1368
1369    #[test]
1370    fn test_run_result_handled() {
1371        let result = RunResult::Handled("output".into());
1372        assert!(result.is_handled());
1373        assert!(!result.is_binary());
1374        assert!(!result.is_silent());
1375        assert_eq!(result.output(), Some("output"));
1376        assert!(result.matches().is_none());
1377    }
1378
1379    #[test]
1380    fn test_run_result_silent() {
1381        let result = RunResult::Silent;
1382        assert!(!result.is_handled());
1383        assert!(!result.is_binary());
1384        assert!(result.is_silent());
1385    }
1386
1387    #[test]
1388    fn test_run_result_binary() {
1389        let bytes = vec![0x25, 0x50, 0x44, 0x46];
1390        let result = RunResult::Binary(bytes.clone(), "report.pdf".into());
1391        assert!(!result.is_handled());
1392        assert!(result.is_binary());
1393        assert!(!result.is_silent());
1394
1395        let (data, filename) = result.binary().unwrap();
1396        assert_eq!(data, &bytes);
1397        assert_eq!(filename, "report.pdf");
1398    }
1399
1400    #[test]
1401    fn test_run_result_no_match() {
1402        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1403        let result = RunResult::NoMatch(matches);
1404        assert!(!result.is_handled());
1405        assert!(!result.is_binary());
1406        assert!(result.matches().is_some());
1407    }
1408
1409    #[test]
1410    fn test_fn_handler() {
1411        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1412            Ok(Output::Render(json!({"status": "ok"})))
1413        });
1414
1415        let ctx = CommandContext::default();
1416        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1417
1418        let result = handler.handle(&matches, &ctx);
1419        assert!(result.is_ok());
1420    }
1421
1422    #[test]
1423    fn test_fn_handler_mutation() {
1424        let mut counter = 0u32;
1425
1426        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1427            counter += 1;
1428            Ok(Output::Render(counter))
1429        });
1430
1431        let ctx = CommandContext::default();
1432        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1433
1434        let _ = handler.handle(&matches, &ctx);
1435        let _ = handler.handle(&matches, &ctx);
1436        let result = handler.handle(&matches, &ctx);
1437
1438        assert!(result.is_ok());
1439        if let Ok(Output::Render(count)) = result {
1440            assert_eq!(count, 3);
1441        }
1442    }
1443
1444    // IntoHandlerResult tests
1445    #[test]
1446    fn test_into_handler_result_from_result_ok() {
1447        use super::IntoHandlerResult;
1448
1449        let result: Result<String, anyhow::Error> = Ok("hello".to_string());
1450        let handler_result = result.into_handler_result();
1451
1452        assert!(handler_result.is_ok());
1453        match handler_result.unwrap() {
1454            Output::Render(s) => assert_eq!(s, "hello"),
1455            _ => panic!("Expected Output::Render"),
1456        }
1457    }
1458
1459    #[test]
1460    fn test_into_handler_result_from_result_err() {
1461        use super::IntoHandlerResult;
1462
1463        let result: Result<String, anyhow::Error> = Err(anyhow::anyhow!("test error"));
1464        let handler_result = result.into_handler_result();
1465
1466        assert!(handler_result.is_err());
1467        assert!(handler_result
1468            .unwrap_err()
1469            .to_string()
1470            .contains("test error"));
1471    }
1472
1473    #[test]
1474    fn test_into_handler_result_passthrough_render() {
1475        use super::IntoHandlerResult;
1476
1477        let handler_result: HandlerResult<String> = Ok(Output::Render("hello".to_string()));
1478        let result = handler_result.into_handler_result();
1479
1480        assert!(result.is_ok());
1481        match result.unwrap() {
1482            Output::Render(s) => assert_eq!(s, "hello"),
1483            _ => panic!("Expected Output::Render"),
1484        }
1485    }
1486
1487    #[test]
1488    fn test_into_handler_result_passthrough_silent() {
1489        use super::IntoHandlerResult;
1490
1491        let handler_result: HandlerResult<String> = Ok(Output::Silent);
1492        let result = handler_result.into_handler_result();
1493
1494        assert!(result.is_ok());
1495        assert!(matches!(result.unwrap(), Output::Silent));
1496    }
1497
1498    #[test]
1499    fn test_into_handler_result_passthrough_binary() {
1500        use super::IntoHandlerResult;
1501
1502        let handler_result: HandlerResult<String> = Ok(Output::Binary {
1503            data: vec![1, 2, 3],
1504            filename: "test.bin".to_string(),
1505        });
1506        let result = handler_result.into_handler_result();
1507
1508        assert!(result.is_ok());
1509        match result.unwrap() {
1510            Output::Binary { data, filename } => {
1511                assert_eq!(data, vec![1, 2, 3]);
1512                assert_eq!(filename, "test.bin");
1513            }
1514            _ => panic!("Expected Output::Binary"),
1515        }
1516    }
1517
1518    #[test]
1519    fn test_fn_handler_with_auto_wrap() {
1520        // Handler that returns Result<T, E> directly (not HandlerResult)
1521        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1522            Ok::<_, anyhow::Error>("auto-wrapped".to_string())
1523        });
1524
1525        let ctx = CommandContext::default();
1526        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1527
1528        let result = handler.handle(&matches, &ctx);
1529        assert!(result.is_ok());
1530        match result.unwrap() {
1531            Output::Render(s) => assert_eq!(s, "auto-wrapped"),
1532            _ => panic!("Expected Output::Render"),
1533        }
1534    }
1535
1536    #[test]
1537    fn test_fn_handler_with_explicit_output() {
1538        // Handler that returns HandlerResult directly (for Silent/Binary)
1539        let mut handler =
1540            FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| Ok(Output::<()>::Silent));
1541
1542        let ctx = CommandContext::default();
1543        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1544
1545        let result = handler.handle(&matches, &ctx);
1546        assert!(result.is_ok());
1547        assert!(matches!(result.unwrap(), Output::Silent));
1548    }
1549
1550    #[test]
1551    fn test_fn_handler_with_custom_error_type() {
1552        // Custom error type that implements Into<anyhow::Error>
1553        #[derive(Debug)]
1554        struct CustomError(String);
1555
1556        impl std::fmt::Display for CustomError {
1557            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1558                write!(f, "CustomError: {}", self.0)
1559            }
1560        }
1561
1562        impl std::error::Error for CustomError {}
1563
1564        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1565            Err::<String, CustomError>(CustomError("oops".to_string()))
1566        });
1567
1568        let ctx = CommandContext::default();
1569        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1570
1571        let result = handler.handle(&matches, &ctx);
1572        assert!(result.is_err());
1573        assert!(result
1574            .unwrap_err()
1575            .to_string()
1576            .contains("CustomError: oops"));
1577    }
1578
1579    // SimpleFnHandler tests (no CommandContext)
1580    #[test]
1581    fn test_simple_fn_handler_basic() {
1582        use super::SimpleFnHandler;
1583
1584        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1585            Ok::<_, anyhow::Error>("no context needed".to_string())
1586        });
1587
1588        let ctx = CommandContext::default();
1589        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1590
1591        let result = handler.handle(&matches, &ctx);
1592        assert!(result.is_ok());
1593        match result.unwrap() {
1594            Output::Render(s) => assert_eq!(s, "no context needed"),
1595            _ => panic!("Expected Output::Render"),
1596        }
1597    }
1598
1599    #[test]
1600    fn test_simple_fn_handler_with_args() {
1601        use super::SimpleFnHandler;
1602
1603        let mut handler = SimpleFnHandler::new(|m: &ArgMatches| {
1604            let verbose = m.get_flag("verbose");
1605            Ok::<_, anyhow::Error>(verbose)
1606        });
1607
1608        let ctx = CommandContext::default();
1609        let matches = clap::Command::new("test")
1610            .arg(
1611                clap::Arg::new("verbose")
1612                    .short('v')
1613                    .action(clap::ArgAction::SetTrue),
1614            )
1615            .get_matches_from(vec!["test", "-v"]);
1616
1617        let result = handler.handle(&matches, &ctx);
1618        assert!(result.is_ok());
1619        match result.unwrap() {
1620            Output::Render(v) => assert!(v),
1621            _ => panic!("Expected Output::Render"),
1622        }
1623    }
1624
1625    #[test]
1626    fn test_simple_fn_handler_explicit_output() {
1627        use super::SimpleFnHandler;
1628
1629        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| Ok(Output::<()>::Silent));
1630
1631        let ctx = CommandContext::default();
1632        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1633
1634        let result = handler.handle(&matches, &ctx);
1635        assert!(result.is_ok());
1636        assert!(matches!(result.unwrap(), Output::Silent));
1637    }
1638
1639    #[test]
1640    fn test_simple_fn_handler_error() {
1641        use super::SimpleFnHandler;
1642
1643        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1644            Err::<String, _>(anyhow::anyhow!("simple error"))
1645        });
1646
1647        let ctx = CommandContext::default();
1648        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1649
1650        let result = handler.handle(&matches, &ctx);
1651        assert!(result.is_err());
1652        assert!(result.unwrap_err().to_string().contains("simple error"));
1653    }
1654
1655    #[test]
1656    fn test_simple_fn_handler_mutation() {
1657        use super::SimpleFnHandler;
1658
1659        let mut counter = 0u32;
1660        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1661            counter += 1;
1662            Ok::<_, anyhow::Error>(counter)
1663        });
1664
1665        let ctx = CommandContext::default();
1666        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1667
1668        let _ = handler.handle(&matches, &ctx);
1669        let _ = handler.handle(&matches, &ctx);
1670        let result = handler.handle(&matches, &ctx);
1671
1672        assert!(result.is_ok());
1673        match result.unwrap() {
1674            Output::Render(n) => assert_eq!(n, 3),
1675            _ => panic!("Expected Output::Render"),
1676        }
1677    }
1678}