Skip to main content

standout_dispatch/
handler.rs

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