Skip to main content

standout_dispatch/
handler.rs

1//! Command handler types.
2//!
3//! This module provides the core types for building logic handlers - the
4//! business logic layer in the dispatch pipeline.
5//!
6//! # Design Rationale
7//!
8//! Logic handlers are responsible for business logic only. They:
9//!
10//! - Receive parsed CLI arguments (`&ArgMatches`) and execution context
11//! - Perform application logic (database queries, file operations, etc.)
12//! - Return serializable data that will be passed to the render handler
13//!
14//! Handlers explicitly do not handle:
15//! - Output formatting (that's the render handler's job)
16//! - Template selection (that's configured at the framework level)
17//! - Theme/style decisions (that's the render handler's job)
18//!
19//! This separation keeps handlers focused and testable - you can unit test
20//! a handler by checking the data it returns, without worrying about rendering.
21//!
22//! # State Management: App State vs Extensions
23//!
24//! [`CommandContext`] provides two mechanisms for state injection:
25//!
26//! | Field | Mutability | Lifetime | Purpose |
27//! |-------|------------|----------|---------|
28//! | `app_state` | Immutable (`&`) | App lifetime (shared via Arc) | Database, Config, API clients |
29//! | `extensions` | Mutable (`&mut`) | Request lifetime | Per-request state, user scope |
30//!
31//! **App State** is configured at app build time via `AppBuilder::app_state()` and shared
32//! immutably across all command invocations. Use for long-lived resources:
33//!
34//! ```rust,ignore
35//! // At app build time
36//! App::builder()
37//!     .app_state(Database::connect()?)
38//!     .app_state(Config::load()?)
39//!     .build()?
40//!
41//! // In handlers
42//! fn list_handler(matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Vec<User>> {
43//!     let db = ctx.app_state.get_required::<Database>()?;
44//!     Ok(Output::Render(db.list_users()?))
45//! }
46//! ```
47//!
48//! **Extensions** are injected per-request by pre-dispatch hooks. Use for request-scoped data:
49//!
50//! ```rust,ignore
51//! Hooks::new().pre_dispatch(|matches, ctx| {
52//!     let user_id = matches.get_one::<String>("user").unwrap();
53//!     ctx.extensions.insert(UserScope { user_id: user_id.clone() });
54//!     Ok(())
55//! })
56//! ```
57//!
58//! # Core Types
59//!
60//! - [`CommandContext`]: Environment information passed to handlers
61//! - [`Extensions`]: Type-safe container for injecting custom state
62//! - [`Output`]: What a handler produces (render data, silent, or binary)
63//! - [`HandlerResult`]: The result type for handlers (`Result<Output<T>, Error>`)
64//! - [`RunResult`]: The result of running the CLI dispatcher
65//! - [`Handler`]: Trait for command handlers (`&mut self`)
66
67use clap::ArgMatches;
68use serde::Serialize;
69use std::any::{Any, TypeId};
70use std::collections::HashMap;
71use std::fmt;
72use std::rc::Rc;
73
74/// Type-safe container for injecting custom state into handlers.
75///
76/// Extensions allow pre-dispatch hooks to inject state that handlers can retrieve.
77/// This enables dependency injection without modifying handler signatures.
78///
79/// # Warning: Clone Behavior
80///
81/// `Extensions` is **not** cloned when the container is cloned. Cloning an `Extensions` instance
82/// results in a new, empty map. This is because the underlying `Box<dyn Any>` values cannot
83/// be cloned generically.
84///
85/// If you need to share state across threads/clones, use `Arc<T>` inside the extension.
86///
87/// # Example
88///
89/// ```rust
90/// use standout_dispatch::{Extensions, CommandContext};
91///
92/// // Define your state types
93/// struct ApiClient { base_url: String }
94/// struct UserScope { user_id: u64 }
95///
96/// // In a pre-dispatch hook, inject state
97/// let mut ctx = CommandContext::default();
98/// ctx.extensions.insert(ApiClient { base_url: "https://api.example.com".into() });
99/// ctx.extensions.insert(UserScope { user_id: 42 });
100///
101/// // In a handler, retrieve state
102/// let api = ctx.extensions.get_required::<ApiClient>()?;
103/// println!("API base: {}", api.base_url);
104/// # Ok::<(), anyhow::Error>(())
105/// ```
106#[derive(Default)]
107pub struct Extensions {
108    map: HashMap<TypeId, Box<dyn Any>>,
109}
110
111impl Extensions {
112    /// Creates a new empty extensions container.
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    /// Inserts a value into the extensions.
118    ///
119    /// If a value of this type already exists, it is replaced and returned.
120    pub fn insert<T: 'static>(&mut self, val: T) -> Option<T> {
121        self.map
122            .insert(TypeId::of::<T>(), Box::new(val))
123            .and_then(|boxed| boxed.downcast().ok().map(|b| *b))
124    }
125
126    /// Gets a reference to a value of the specified type.
127    ///
128    /// Returns `None` if no value of this type exists.
129    pub fn get<T: 'static>(&self) -> Option<&T> {
130        self.map
131            .get(&TypeId::of::<T>())
132            .and_then(|boxed| boxed.downcast_ref())
133    }
134
135    /// Gets a mutable reference to a value of the specified type.
136    ///
137    /// Returns `None` if no value of this type exists.
138    pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
139        self.map
140            .get_mut(&TypeId::of::<T>())
141            .and_then(|boxed| boxed.downcast_mut())
142    }
143
144    /// Gets a required reference to a value of the specified type.
145    ///
146    /// Returns an error if no value of this type exists.
147    pub fn get_required<T: 'static>(&self) -> Result<&T, anyhow::Error> {
148        self.get::<T>().ok_or_else(|| {
149            anyhow::anyhow!(
150                "Extension missing: type {} not found in context",
151                std::any::type_name::<T>()
152            )
153        })
154    }
155
156    /// Gets a required mutable reference to a value of the specified type.
157    ///
158    /// Returns an error if no value of this type exists.
159    pub fn get_mut_required<T: 'static>(&mut self) -> Result<&mut T, anyhow::Error> {
160        self.get_mut::<T>().ok_or_else(|| {
161            anyhow::anyhow!(
162                "Extension missing: type {} not found in context",
163                std::any::type_name::<T>()
164            )
165        })
166    }
167
168    /// Removes a value of the specified type, returning it if it existed.
169    pub fn remove<T: 'static>(&mut self) -> Option<T> {
170        self.map
171            .remove(&TypeId::of::<T>())
172            .and_then(|boxed| boxed.downcast().ok().map(|b| *b))
173    }
174
175    /// Returns `true` if the extensions contain a value of the specified type.
176    pub fn contains<T: 'static>(&self) -> bool {
177        self.map.contains_key(&TypeId::of::<T>())
178    }
179
180    /// Returns the number of extensions stored.
181    pub fn len(&self) -> usize {
182        self.map.len()
183    }
184
185    /// Returns `true` if no extensions are stored.
186    pub fn is_empty(&self) -> bool {
187        self.map.is_empty()
188    }
189
190    /// Removes all extensions.
191    pub fn clear(&mut self) {
192        self.map.clear();
193    }
194}
195
196impl fmt::Debug for Extensions {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        f.debug_struct("Extensions")
199            .field("len", &self.map.len())
200            .finish_non_exhaustive()
201    }
202}
203
204impl Clone for Extensions {
205    fn clone(&self) -> Self {
206        // Extensions cannot be cloned because Box<dyn Any> isn't Clone.
207        // Return empty extensions on clone - this is a limitation but
208        // matches the behavior of http::Extensions.
209        Self::new()
210    }
211}
212
213/// Context passed to command handlers.
214///
215/// Provides information about the execution environment plus two mechanisms
216/// for state injection:
217///
218/// - **`app_state`**: Immutable, app-lifetime state (Database, Config, API clients)
219/// - **`extensions`**: Mutable, per-request state (UserScope, RequestId)
220///
221/// Note that output format is deliberately not included here - format decisions
222/// are made by the render handler, not by logic handlers.
223///
224/// # App State (Immutable, Shared)
225///
226/// App state is configured at build time and shared across all dispatches:
227///
228/// ```rust,ignore
229/// use standout::cli::App;
230///
231/// struct Database { /* ... */ }
232/// struct Config { api_url: String }
233///
234/// App::builder()
235///     .app_state(Database::connect()?)
236///     .app_state(Config { api_url: "https://api.example.com".into() })
237///     .command("list", list_handler, "{{ items }}")
238///     .build()?
239/// ```
240///
241/// Handlers retrieve app state immutably:
242///
243/// ```rust,ignore
244/// fn list_handler(matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Vec<Item>> {
245///     let db = ctx.app_state.get_required::<Database>()?;
246///     let config = ctx.app_state.get_required::<Config>()?;
247///     Ok(Output::Render(db.list_items(&config.api_url)?))
248/// }
249/// ```
250///
251/// ## Shared Mutable State
252///
253/// Since `app_state` is shared via `Arc`, it is immutable by default. To share mutable state
254/// (like counters or caches), use interior mutability primitives like `RwLock`, `Mutex`, or atomic types:
255///
256/// ```rust,ignore
257/// use std::sync::atomic::AtomicUsize;
258///
259/// struct Metrics { request_count: AtomicUsize }
260///
261/// // Builder
262/// App::builder().app_state(Metrics { request_count: AtomicUsize::new(0) });
263///
264/// // Handler
265/// let metrics = ctx.app_state.get_required::<Metrics>()?;
266/// metrics.request_count.fetch_add(1, Ordering::Relaxed);
267/// ```
268///
269/// # Extensions (Mutable, Per-Request)
270///
271/// Pre-dispatch hooks inject per-request state into `extensions`:
272///
273/// ```rust
274/// use standout_dispatch::{Hooks, HookError, CommandContext};
275///
276/// struct UserScope { user_id: String }
277///
278/// let hooks = Hooks::new()
279///     .pre_dispatch(|matches, ctx| {
280///         let user_id = matches.get_one::<String>("user").unwrap();
281///         ctx.extensions.insert(UserScope { user_id: user_id.clone() });
282///         Ok(())
283///     });
284///
285/// // In handler:
286/// fn my_handler(matches: &clap::ArgMatches, ctx: &CommandContext) -> anyhow::Result<()> {
287///     let scope = ctx.extensions.get_required::<UserScope>()?;
288///     // use scope.user_id...
289///     Ok(())
290/// }
291/// ```
292#[derive(Debug)]
293pub struct CommandContext {
294    /// The command path being executed (e.g., ["config", "get"])
295    pub command_path: Vec<String>,
296
297    /// Immutable app-level state shared across all dispatches.
298    ///
299    /// Configured via `AppBuilder::app_state()`. Contains long-lived resources
300    /// like database connections, configuration, and API clients.
301    ///
302    /// Use `get::<T>()` or `get_required::<T>()` to retrieve values.
303    pub app_state: Rc<Extensions>,
304
305    /// Mutable per-request state container.
306    ///
307    /// Pre-dispatch hooks can insert values that handlers retrieve.
308    /// Each dispatch gets a fresh Extensions instance.
309    pub extensions: Extensions,
310}
311
312impl CommandContext {
313    /// Creates a new CommandContext with the given path and shared app state.
314    ///
315    /// This is more efficient than `Default::default()` when you already have app_state.
316    pub fn new(command_path: Vec<String>, app_state: Rc<Extensions>) -> Self {
317        Self {
318            command_path,
319            app_state,
320            extensions: Extensions::new(),
321        }
322    }
323}
324
325impl Default for CommandContext {
326    fn default() -> Self {
327        Self {
328            command_path: Vec::new(),
329            app_state: Rc::new(Extensions::new()),
330            extensions: Extensions::new(),
331        }
332    }
333}
334
335/// What a handler produces.
336///
337/// This enum represents the different types of output a command handler can produce.
338#[derive(Debug)]
339pub enum Output<T: Serialize> {
340    /// Data to render with a template or serialize to JSON/YAML/etc.
341    Render(T),
342    /// Silent exit (no output produced)
343    Silent,
344    /// Binary output for file exports
345    Binary {
346        /// The binary data
347        data: Vec<u8>,
348        /// Suggested filename for the output
349        filename: String,
350    },
351}
352
353impl<T: Serialize> Output<T> {
354    /// Returns true if this is a render result.
355    pub fn is_render(&self) -> bool {
356        matches!(self, Output::Render(_))
357    }
358
359    /// Returns true if this is a silent result.
360    pub fn is_silent(&self) -> bool {
361        matches!(self, Output::Silent)
362    }
363
364    /// Returns true if this is a binary result.
365    pub fn is_binary(&self) -> bool {
366        matches!(self, Output::Binary { .. })
367    }
368}
369
370/// The result type for command handlers.
371///
372/// Enables use of the `?` operator for error propagation.
373pub type HandlerResult<T> = Result<Output<T>, anyhow::Error>;
374
375/// Trait for types that can be converted into a [`HandlerResult`].
376///
377/// This enables handlers to return either `Result<T, E>` directly (auto-wrapped
378/// in [`Output::Render`]) or the explicit [`HandlerResult<T>`] when fine-grained
379/// control is needed (for [`Output::Silent`] or [`Output::Binary`]).
380///
381/// # Example
382///
383/// ```rust
384/// use standout_dispatch::{HandlerResult, Output, IntoHandlerResult};
385///
386/// // Direct Result<T, E> is auto-wrapped in Output::Render
387/// fn simple() -> Result<String, anyhow::Error> {
388///     Ok("hello".to_string())
389/// }
390/// let result: HandlerResult<String> = simple().into_handler_result();
391/// assert!(matches!(result, Ok(Output::Render(_))));
392///
393/// // HandlerResult<T> passes through unchanged
394/// fn explicit() -> HandlerResult<String> {
395///     Ok(Output::Silent)
396/// }
397/// let result: HandlerResult<String> = explicit().into_handler_result();
398/// assert!(matches!(result, Ok(Output::Silent)));
399/// ```
400pub trait IntoHandlerResult<T: Serialize> {
401    /// Convert this type into a [`HandlerResult<T>`].
402    fn into_handler_result(self) -> HandlerResult<T>;
403}
404
405/// Implementation for `Result<T, E>` - auto-wraps successful values in [`Output::Render`].
406///
407/// This is the ergonomic path: handlers can return `Result<T, E>` directly
408/// and the framework wraps it appropriately.
409impl<T, E> IntoHandlerResult<T> for Result<T, E>
410where
411    T: Serialize,
412    E: Into<anyhow::Error>,
413{
414    fn into_handler_result(self) -> HandlerResult<T> {
415        self.map(Output::Render).map_err(Into::into)
416    }
417}
418
419/// Implementation for `HandlerResult<T>` - passes through unchanged.
420///
421/// This is the explicit path: handlers that need [`Output::Silent`] or
422/// [`Output::Binary`] can return `HandlerResult<T>` directly.
423impl<T: Serialize> IntoHandlerResult<T> for HandlerResult<T> {
424    fn into_handler_result(self) -> HandlerResult<T> {
425        self
426    }
427}
428
429/// Result of running the CLI dispatcher.
430///
431/// After processing arguments, the dispatcher either handles a command
432/// or falls through for manual handling.
433#[derive(Debug)]
434pub enum RunResult {
435    /// A handler processed the command; contains the rendered output
436    Handled(String),
437    /// A handler produced binary output (bytes, suggested filename)
438    Binary(Vec<u8>, String),
439    /// Silent output (handler completed but produced no output)
440    Silent,
441    /// No handler matched; contains the ArgMatches for manual handling
442    NoMatch(ArgMatches),
443}
444
445impl RunResult {
446    /// Returns true if a handler processed the command (text output).
447    pub fn is_handled(&self) -> bool {
448        matches!(self, RunResult::Handled(_))
449    }
450
451    /// Returns true if the result is binary output.
452    pub fn is_binary(&self) -> bool {
453        matches!(self, RunResult::Binary(_, _))
454    }
455
456    /// Returns true if the result is silent.
457    pub fn is_silent(&self) -> bool {
458        matches!(self, RunResult::Silent)
459    }
460
461    /// Returns the output if handled, or None otherwise.
462    pub fn output(&self) -> Option<&str> {
463        match self {
464            RunResult::Handled(s) => Some(s),
465            _ => None,
466        }
467    }
468
469    /// Returns the binary data and filename if binary, or None otherwise.
470    pub fn binary(&self) -> Option<(&[u8], &str)> {
471        match self {
472            RunResult::Binary(bytes, filename) => Some((bytes, filename)),
473            _ => None,
474        }
475    }
476
477    /// Returns the matches if unhandled, or None if handled.
478    pub fn matches(&self) -> Option<&ArgMatches> {
479        match self {
480            RunResult::NoMatch(m) => Some(m),
481            _ => None,
482        }
483    }
484}
485
486/// Trait for command handlers.
487///
488/// Handlers take `&mut self` allowing direct mutation of internal state.
489/// This is the common case for CLI applications which are single-threaded.
490///
491/// # Example
492///
493/// ```rust
494/// use standout_dispatch::{Handler, HandlerResult, Output, CommandContext};
495/// use clap::ArgMatches;
496/// use serde::Serialize;
497///
498/// struct Counter { count: u32 }
499///
500/// impl Handler for Counter {
501///     type Output = u32;
502///
503///     fn handle(&mut self, _m: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<u32> {
504///         self.count += 1;
505///         Ok(Output::Render(self.count))
506///     }
507/// }
508/// ```
509pub trait Handler {
510    /// The output type produced by this handler (must be serializable)
511    type Output: Serialize;
512
513    /// Execute the handler with the given matches and context.
514    fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext)
515        -> HandlerResult<Self::Output>;
516}
517
518/// A wrapper that implements Handler for FnMut closures.
519///
520/// The closure can return either:
521/// - `Result<T, E>` - automatically wrapped in [`Output::Render`]
522/// - `HandlerResult<T>` - passed through unchanged (for [`Output::Silent`] or [`Output::Binary`])
523///
524/// # Example
525///
526/// ```rust
527/// use standout_dispatch::{FnHandler, Handler, CommandContext, Output};
528/// use clap::ArgMatches;
529///
530/// // Returning Result<T, E> directly (auto-wrapped)
531/// let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
532///     Ok::<_, anyhow::Error>("hello".to_string())
533/// });
534///
535/// // Returning HandlerResult<T> explicitly (for Silent/Binary)
536/// let mut silent_handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
537///     Ok(Output::<()>::Silent)
538/// });
539/// ```
540pub struct FnHandler<F, T, R = HandlerResult<T>>
541where
542    T: Serialize,
543{
544    f: F,
545    _phantom: std::marker::PhantomData<fn() -> (T, R)>,
546}
547
548impl<F, T, R> FnHandler<F, T, R>
549where
550    F: FnMut(&ArgMatches, &CommandContext) -> R,
551    R: IntoHandlerResult<T>,
552    T: Serialize,
553{
554    /// Creates a new FnHandler wrapping the given FnMut closure.
555    pub fn new(f: F) -> Self {
556        Self {
557            f,
558            _phantom: std::marker::PhantomData,
559        }
560    }
561}
562
563impl<F, T, R> Handler for FnHandler<F, T, R>
564where
565    F: FnMut(&ArgMatches, &CommandContext) -> R,
566    R: IntoHandlerResult<T>,
567    T: Serialize,
568{
569    type Output = T;
570
571    fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<T> {
572        (self.f)(matches, ctx).into_handler_result()
573    }
574}
575
576/// A handler wrapper for functions that don't need [`CommandContext`].
577///
578/// This is the simpler variant of [`FnHandler`] for handlers that only need
579/// [`ArgMatches`]. The context parameter is accepted but ignored internally.
580///
581/// The closure can return either:
582/// - `Result<T, E>` - automatically wrapped in [`Output::Render`]
583/// - `HandlerResult<T>` - passed through unchanged (for [`Output::Silent`] or [`Output::Binary`])
584///
585/// # Example
586///
587/// ```rust
588/// use standout_dispatch::{SimpleFnHandler, Handler, CommandContext, Output};
589/// use clap::ArgMatches;
590///
591/// // Handler that doesn't need context - just uses ArgMatches
592/// let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
593///     Ok::<_, anyhow::Error>("Hello, world!".to_string())
594/// });
595///
596/// // Can still be used via Handler trait (context is ignored)
597/// let ctx = CommandContext::default();
598/// let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
599/// let result = handler.handle(&matches, &ctx);
600/// assert!(matches!(result, Ok(Output::Render(_))));
601/// ```
602pub struct SimpleFnHandler<F, T, R = HandlerResult<T>>
603where
604    T: Serialize,
605{
606    f: F,
607    _phantom: std::marker::PhantomData<fn() -> (T, R)>,
608}
609
610impl<F, T, R> SimpleFnHandler<F, T, R>
611where
612    F: FnMut(&ArgMatches) -> R,
613    R: IntoHandlerResult<T>,
614    T: Serialize,
615{
616    /// Creates a new SimpleFnHandler wrapping the given FnMut closure.
617    pub fn new(f: F) -> Self {
618        Self {
619            f,
620            _phantom: std::marker::PhantomData,
621        }
622    }
623}
624
625impl<F, T, R> Handler for SimpleFnHandler<F, T, R>
626where
627    F: FnMut(&ArgMatches) -> R,
628    R: IntoHandlerResult<T>,
629    T: Serialize,
630{
631    type Output = T;
632
633    fn handle(&mut self, matches: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<T> {
634        (self.f)(matches).into_handler_result()
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641    use serde_json::json;
642
643    #[test]
644    fn test_command_context_creation() {
645        let ctx = CommandContext {
646            command_path: vec!["config".into(), "get".into()],
647            app_state: Rc::new(Extensions::new()),
648            extensions: Extensions::new(),
649        };
650        assert_eq!(ctx.command_path, vec!["config", "get"]);
651    }
652
653    #[test]
654    fn test_command_context_default() {
655        let ctx = CommandContext::default();
656        assert!(ctx.command_path.is_empty());
657        assert!(ctx.extensions.is_empty());
658        assert!(ctx.app_state.is_empty());
659    }
660
661    #[test]
662    fn test_command_context_with_app_state() {
663        struct Database {
664            url: String,
665        }
666        struct Config {
667            debug: bool,
668        }
669
670        // Build app state
671        let mut app_state = Extensions::new();
672        app_state.insert(Database {
673            url: "postgres://localhost".into(),
674        });
675        app_state.insert(Config { debug: true });
676        let app_state = Rc::new(app_state);
677
678        // Create context with app state
679        let ctx = CommandContext {
680            command_path: vec!["list".into()],
681            app_state: app_state.clone(),
682            extensions: Extensions::new(),
683        };
684
685        // Retrieve app state
686        let db = ctx.app_state.get::<Database>().unwrap();
687        assert_eq!(db.url, "postgres://localhost");
688
689        let config = ctx.app_state.get::<Config>().unwrap();
690        assert!(config.debug);
691
692        // App state is shared via Rc
693        assert_eq!(Rc::strong_count(&ctx.app_state), 2);
694    }
695
696    #[test]
697    fn test_command_context_app_state_get_required() {
698        struct Present;
699
700        let mut app_state = Extensions::new();
701        app_state.insert(Present);
702
703        let ctx = CommandContext {
704            command_path: vec![],
705            app_state: Rc::new(app_state),
706            extensions: Extensions::new(),
707        };
708
709        // Success case
710        assert!(ctx.app_state.get_required::<Present>().is_ok());
711
712        // Failure case
713        #[derive(Debug)]
714        struct Missing;
715        let err = ctx.app_state.get_required::<Missing>();
716        assert!(err.is_err());
717        assert!(err.unwrap_err().to_string().contains("Extension missing"));
718    }
719
720    // Extensions tests
721    #[test]
722    fn test_extensions_insert_and_get() {
723        struct MyState {
724            value: i32,
725        }
726
727        let mut ext = Extensions::new();
728        assert!(ext.is_empty());
729
730        ext.insert(MyState { value: 42 });
731        assert!(!ext.is_empty());
732        assert_eq!(ext.len(), 1);
733
734        let state = ext.get::<MyState>().unwrap();
735        assert_eq!(state.value, 42);
736    }
737
738    #[test]
739    fn test_extensions_get_mut() {
740        struct Counter {
741            count: i32,
742        }
743
744        let mut ext = Extensions::new();
745        ext.insert(Counter { count: 0 });
746
747        if let Some(counter) = ext.get_mut::<Counter>() {
748            counter.count += 1;
749        }
750
751        assert_eq!(ext.get::<Counter>().unwrap().count, 1);
752    }
753
754    #[test]
755    fn test_extensions_multiple_types() {
756        struct TypeA(i32);
757        struct TypeB(String);
758
759        let mut ext = Extensions::new();
760        ext.insert(TypeA(1));
761        ext.insert(TypeB("hello".into()));
762
763        assert_eq!(ext.len(), 2);
764        assert_eq!(ext.get::<TypeA>().unwrap().0, 1);
765        assert_eq!(ext.get::<TypeB>().unwrap().0, "hello");
766    }
767
768    #[test]
769    fn test_extensions_replace() {
770        struct Value(i32);
771
772        let mut ext = Extensions::new();
773        ext.insert(Value(1));
774
775        let old = ext.insert(Value(2));
776        assert_eq!(old.unwrap().0, 1);
777        assert_eq!(ext.get::<Value>().unwrap().0, 2);
778    }
779
780    #[test]
781    fn test_extensions_remove() {
782        struct Value(i32);
783
784        let mut ext = Extensions::new();
785        ext.insert(Value(42));
786
787        let removed = ext.remove::<Value>();
788        assert_eq!(removed.unwrap().0, 42);
789        assert!(ext.is_empty());
790        assert!(ext.get::<Value>().is_none());
791    }
792
793    #[test]
794    fn test_extensions_contains() {
795        struct Present;
796        struct Absent;
797
798        let mut ext = Extensions::new();
799        ext.insert(Present);
800
801        assert!(ext.contains::<Present>());
802        assert!(!ext.contains::<Absent>());
803    }
804
805    #[test]
806    fn test_extensions_clear() {
807        struct A;
808        struct B;
809
810        let mut ext = Extensions::new();
811        ext.insert(A);
812        ext.insert(B);
813        assert_eq!(ext.len(), 2);
814
815        ext.clear();
816        assert!(ext.is_empty());
817    }
818
819    #[test]
820    fn test_extensions_missing_type_returns_none() {
821        struct NotInserted;
822
823        let ext = Extensions::new();
824        assert!(ext.get::<NotInserted>().is_none());
825    }
826
827    #[test]
828    fn test_extensions_get_required() {
829        #[derive(Debug)]
830        struct Config {
831            value: i32,
832        }
833
834        let mut ext = Extensions::new();
835        ext.insert(Config { value: 100 });
836
837        // Success case
838        let val = ext.get_required::<Config>();
839        assert!(val.is_ok());
840        assert_eq!(val.unwrap().value, 100);
841
842        // Failure case
843        #[derive(Debug)]
844        struct Missing;
845        let err = ext.get_required::<Missing>();
846        assert!(err.is_err());
847        assert!(err
848            .unwrap_err()
849            .to_string()
850            .contains("Extension missing: type"));
851    }
852
853    #[test]
854    fn test_extensions_get_mut_required() {
855        #[derive(Debug)]
856        struct State {
857            count: i32,
858        }
859
860        let mut ext = Extensions::new();
861        ext.insert(State { count: 0 });
862
863        // Success case
864        {
865            let val = ext.get_mut_required::<State>();
866            assert!(val.is_ok());
867            val.unwrap().count += 1;
868        }
869        assert_eq!(ext.get_required::<State>().unwrap().count, 1);
870
871        // Failure case
872        #[derive(Debug)]
873        struct Missing;
874        let err = ext.get_mut_required::<Missing>();
875        assert!(err.is_err());
876    }
877
878    #[test]
879    fn test_extensions_clone_behavior() {
880        // Verify the documented behavior that Clone drops extensions
881        struct Data(i32);
882
883        let mut original = Extensions::new();
884        original.insert(Data(42));
885
886        let cloned = original.clone();
887
888        // Original has data
889        assert!(original.get::<Data>().is_some());
890
891        // Cloned is empty
892        assert!(cloned.is_empty());
893        assert!(cloned.get::<Data>().is_none());
894    }
895
896    #[test]
897    fn test_output_render() {
898        let output: Output<String> = Output::Render("success".into());
899        assert!(output.is_render());
900        assert!(!output.is_silent());
901        assert!(!output.is_binary());
902    }
903
904    #[test]
905    fn test_output_silent() {
906        let output: Output<String> = Output::Silent;
907        assert!(!output.is_render());
908        assert!(output.is_silent());
909        assert!(!output.is_binary());
910    }
911
912    #[test]
913    fn test_output_binary() {
914        let output: Output<String> = Output::Binary {
915            data: vec![0x25, 0x50, 0x44, 0x46],
916            filename: "report.pdf".into(),
917        };
918        assert!(!output.is_render());
919        assert!(!output.is_silent());
920        assert!(output.is_binary());
921    }
922
923    #[test]
924    fn test_run_result_handled() {
925        let result = RunResult::Handled("output".into());
926        assert!(result.is_handled());
927        assert!(!result.is_binary());
928        assert!(!result.is_silent());
929        assert_eq!(result.output(), Some("output"));
930        assert!(result.matches().is_none());
931    }
932
933    #[test]
934    fn test_run_result_silent() {
935        let result = RunResult::Silent;
936        assert!(!result.is_handled());
937        assert!(!result.is_binary());
938        assert!(result.is_silent());
939    }
940
941    #[test]
942    fn test_run_result_binary() {
943        let bytes = vec![0x25, 0x50, 0x44, 0x46];
944        let result = RunResult::Binary(bytes.clone(), "report.pdf".into());
945        assert!(!result.is_handled());
946        assert!(result.is_binary());
947        assert!(!result.is_silent());
948
949        let (data, filename) = result.binary().unwrap();
950        assert_eq!(data, &bytes);
951        assert_eq!(filename, "report.pdf");
952    }
953
954    #[test]
955    fn test_run_result_no_match() {
956        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
957        let result = RunResult::NoMatch(matches);
958        assert!(!result.is_handled());
959        assert!(!result.is_binary());
960        assert!(result.matches().is_some());
961    }
962
963    #[test]
964    fn test_fn_handler() {
965        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
966            Ok(Output::Render(json!({"status": "ok"})))
967        });
968
969        let ctx = CommandContext::default();
970        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
971
972        let result = handler.handle(&matches, &ctx);
973        assert!(result.is_ok());
974    }
975
976    #[test]
977    fn test_fn_handler_mutation() {
978        let mut counter = 0u32;
979
980        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
981            counter += 1;
982            Ok(Output::Render(counter))
983        });
984
985        let ctx = CommandContext::default();
986        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
987
988        let _ = handler.handle(&matches, &ctx);
989        let _ = handler.handle(&matches, &ctx);
990        let result = handler.handle(&matches, &ctx);
991
992        assert!(result.is_ok());
993        if let Ok(Output::Render(count)) = result {
994            assert_eq!(count, 3);
995        }
996    }
997
998    // IntoHandlerResult tests
999    #[test]
1000    fn test_into_handler_result_from_result_ok() {
1001        use super::IntoHandlerResult;
1002
1003        let result: Result<String, anyhow::Error> = Ok("hello".to_string());
1004        let handler_result = result.into_handler_result();
1005
1006        assert!(handler_result.is_ok());
1007        match handler_result.unwrap() {
1008            Output::Render(s) => assert_eq!(s, "hello"),
1009            _ => panic!("Expected Output::Render"),
1010        }
1011    }
1012
1013    #[test]
1014    fn test_into_handler_result_from_result_err() {
1015        use super::IntoHandlerResult;
1016
1017        let result: Result<String, anyhow::Error> = Err(anyhow::anyhow!("test error"));
1018        let handler_result = result.into_handler_result();
1019
1020        assert!(handler_result.is_err());
1021        assert!(handler_result
1022            .unwrap_err()
1023            .to_string()
1024            .contains("test error"));
1025    }
1026
1027    #[test]
1028    fn test_into_handler_result_passthrough_render() {
1029        use super::IntoHandlerResult;
1030
1031        let handler_result: HandlerResult<String> = Ok(Output::Render("hello".to_string()));
1032        let result = handler_result.into_handler_result();
1033
1034        assert!(result.is_ok());
1035        match result.unwrap() {
1036            Output::Render(s) => assert_eq!(s, "hello"),
1037            _ => panic!("Expected Output::Render"),
1038        }
1039    }
1040
1041    #[test]
1042    fn test_into_handler_result_passthrough_silent() {
1043        use super::IntoHandlerResult;
1044
1045        let handler_result: HandlerResult<String> = Ok(Output::Silent);
1046        let result = handler_result.into_handler_result();
1047
1048        assert!(result.is_ok());
1049        assert!(matches!(result.unwrap(), Output::Silent));
1050    }
1051
1052    #[test]
1053    fn test_into_handler_result_passthrough_binary() {
1054        use super::IntoHandlerResult;
1055
1056        let handler_result: HandlerResult<String> = Ok(Output::Binary {
1057            data: vec![1, 2, 3],
1058            filename: "test.bin".to_string(),
1059        });
1060        let result = handler_result.into_handler_result();
1061
1062        assert!(result.is_ok());
1063        match result.unwrap() {
1064            Output::Binary { data, filename } => {
1065                assert_eq!(data, vec![1, 2, 3]);
1066                assert_eq!(filename, "test.bin");
1067            }
1068            _ => panic!("Expected Output::Binary"),
1069        }
1070    }
1071
1072    #[test]
1073    fn test_fn_handler_with_auto_wrap() {
1074        // Handler that returns Result<T, E> directly (not HandlerResult)
1075        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1076            Ok::<_, anyhow::Error>("auto-wrapped".to_string())
1077        });
1078
1079        let ctx = CommandContext::default();
1080        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1081
1082        let result = handler.handle(&matches, &ctx);
1083        assert!(result.is_ok());
1084        match result.unwrap() {
1085            Output::Render(s) => assert_eq!(s, "auto-wrapped"),
1086            _ => panic!("Expected Output::Render"),
1087        }
1088    }
1089
1090    #[test]
1091    fn test_fn_handler_with_explicit_output() {
1092        // Handler that returns HandlerResult directly (for Silent/Binary)
1093        let mut handler =
1094            FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| Ok(Output::<()>::Silent));
1095
1096        let ctx = CommandContext::default();
1097        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1098
1099        let result = handler.handle(&matches, &ctx);
1100        assert!(result.is_ok());
1101        assert!(matches!(result.unwrap(), Output::Silent));
1102    }
1103
1104    #[test]
1105    fn test_fn_handler_with_custom_error_type() {
1106        // Custom error type that implements Into<anyhow::Error>
1107        #[derive(Debug)]
1108        struct CustomError(String);
1109
1110        impl std::fmt::Display for CustomError {
1111            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1112                write!(f, "CustomError: {}", self.0)
1113            }
1114        }
1115
1116        impl std::error::Error for CustomError {}
1117
1118        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1119            Err::<String, CustomError>(CustomError("oops".to_string()))
1120        });
1121
1122        let ctx = CommandContext::default();
1123        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1124
1125        let result = handler.handle(&matches, &ctx);
1126        assert!(result.is_err());
1127        assert!(result
1128            .unwrap_err()
1129            .to_string()
1130            .contains("CustomError: oops"));
1131    }
1132
1133    // SimpleFnHandler tests (no CommandContext)
1134    #[test]
1135    fn test_simple_fn_handler_basic() {
1136        use super::SimpleFnHandler;
1137
1138        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1139            Ok::<_, anyhow::Error>("no context needed".to_string())
1140        });
1141
1142        let ctx = CommandContext::default();
1143        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1144
1145        let result = handler.handle(&matches, &ctx);
1146        assert!(result.is_ok());
1147        match result.unwrap() {
1148            Output::Render(s) => assert_eq!(s, "no context needed"),
1149            _ => panic!("Expected Output::Render"),
1150        }
1151    }
1152
1153    #[test]
1154    fn test_simple_fn_handler_with_args() {
1155        use super::SimpleFnHandler;
1156
1157        let mut handler = SimpleFnHandler::new(|m: &ArgMatches| {
1158            let verbose = m.get_flag("verbose");
1159            Ok::<_, anyhow::Error>(verbose)
1160        });
1161
1162        let ctx = CommandContext::default();
1163        let matches = clap::Command::new("test")
1164            .arg(
1165                clap::Arg::new("verbose")
1166                    .short('v')
1167                    .action(clap::ArgAction::SetTrue),
1168            )
1169            .get_matches_from(vec!["test", "-v"]);
1170
1171        let result = handler.handle(&matches, &ctx);
1172        assert!(result.is_ok());
1173        match result.unwrap() {
1174            Output::Render(v) => assert!(v),
1175            _ => panic!("Expected Output::Render"),
1176        }
1177    }
1178
1179    #[test]
1180    fn test_simple_fn_handler_explicit_output() {
1181        use super::SimpleFnHandler;
1182
1183        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| Ok(Output::<()>::Silent));
1184
1185        let ctx = CommandContext::default();
1186        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1187
1188        let result = handler.handle(&matches, &ctx);
1189        assert!(result.is_ok());
1190        assert!(matches!(result.unwrap(), Output::Silent));
1191    }
1192
1193    #[test]
1194    fn test_simple_fn_handler_error() {
1195        use super::SimpleFnHandler;
1196
1197        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1198            Err::<String, _>(anyhow::anyhow!("simple error"))
1199        });
1200
1201        let ctx = CommandContext::default();
1202        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1203
1204        let result = handler.handle(&matches, &ctx);
1205        assert!(result.is_err());
1206        assert!(result.unwrap_err().to_string().contains("simple error"));
1207    }
1208
1209    #[test]
1210    fn test_simple_fn_handler_mutation() {
1211        use super::SimpleFnHandler;
1212
1213        let mut counter = 0u32;
1214        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1215            counter += 1;
1216            Ok::<_, anyhow::Error>(counter)
1217        });
1218
1219        let ctx = CommandContext::default();
1220        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1221
1222        let _ = handler.handle(&matches, &ctx);
1223        let _ = handler.handle(&matches, &ctx);
1224        let result = handler.handle(&matches, &ctx);
1225
1226        assert!(result.is_ok());
1227        match result.unwrap() {
1228            Output::Render(n) => assert_eq!(n, 3),
1229            _ => panic!("Expected Output::Render"),
1230        }
1231    }
1232}