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