Skip to main content

standout_dispatch/
handler.rs

1use crate::artifact::{Artifact, ArtifactRun};
2use crate::diagnostic::{Diagnostic, Severity};
3use crate::hooks::HookPhase;
4use crate::results::{NoEvents, Results};
5use crate::verify::ExpectedArg;
6use clap::ArgMatches;
7use serde::Serialize;
8use std::any::{Any, TypeId};
9use std::collections::HashMap;
10use std::fmt;
11use std::rc::Rc;
12use std::sync::Arc;
13#[derive(Default)]
14pub struct Extensions {
15    map: HashMap<TypeId, Box<dyn Any>>,
16}
17impl Extensions {
18    pub fn new() -> Self {
19        Self::default()
20    }
21    pub fn insert<T: 'static>(&mut self, val: T) -> Option<T> {
22        self.map
23            .insert(TypeId::of::<T>(), Box::new(val))
24            .and_then(|boxed| boxed.downcast().ok().map(|b| *b))
25    }
26    pub fn get<T: 'static>(&self) -> Option<&T> {
27        self.map
28            .get(&TypeId::of::<T>())
29            .and_then(|boxed| boxed.downcast_ref())
30    }
31    pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
32        self.map
33            .get_mut(&TypeId::of::<T>())
34            .and_then(|boxed| boxed.downcast_mut())
35    }
36    pub fn get_required<T: 'static>(&self) -> Result<&T, anyhow::Error> {
37        self.get::<T>().ok_or_else(|| {
38            anyhow::anyhow!(
39                "Extension missing: type {} not found in context",
40                std::any::type_name::<T>()
41            )
42        })
43    }
44    pub fn get_mut_required<T: 'static>(&mut self) -> Result<&mut T, anyhow::Error> {
45        self.get_mut::<T>().ok_or_else(|| {
46            anyhow::anyhow!(
47                "Extension missing: type {} not found in context",
48                std::any::type_name::<T>()
49            )
50        })
51    }
52    pub fn remove<T: 'static>(&mut self) -> Option<T> {
53        self.map
54            .remove(&TypeId::of::<T>())
55            .and_then(|boxed| boxed.downcast().ok().map(|b| *b))
56    }
57    pub fn contains<T: 'static>(&self) -> bool {
58        self.map.contains_key(&TypeId::of::<T>())
59    }
60    pub fn len(&self) -> usize {
61        self.map.len()
62    }
63    pub fn is_empty(&self) -> bool {
64        self.map.is_empty()
65    }
66    pub fn clear(&mut self) {
67        self.map.clear();
68    }
69}
70impl fmt::Debug for Extensions {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        f.debug_struct("Extensions")
73            .field("len", &self.map.len())
74            .finish_non_exhaustive()
75    }
76}
77impl Clone for Extensions {
78    // `Box<dyn Any>` isn't `Clone`: a clone starts empty.
79    fn clone(&self) -> Self {
80        Self::new()
81    }
82}
83#[derive(Debug)]
84pub struct CommandContext {
85    pub command_path: Vec<String>,
86    pub app_state: Rc<Extensions>,
87    pub extensions: Extensions,
88}
89impl CommandContext {
90    pub fn new(command_path: Vec<String>, app_state: Rc<Extensions>) -> Self {
91        Self {
92            command_path,
93            app_state,
94            extensions: Extensions::new(),
95        }
96    }
97}
98impl Default for CommandContext {
99    fn default() -> Self {
100        Self {
101            command_path: Vec::new(),
102            app_state: Rc::new(Extensions::new()),
103            extensions: Extensions::new(),
104        }
105    }
106}
107#[derive(Debug)]
108#[non_exhaustive]
109pub enum Output<T: Serialize> {
110    Render(T),
111    Silent,
112    Binary {
113        data: Vec<u8>,
114        filename: String,
115    },
116    Artifact(Artifact<T>),
117    /// Emitted as `output` alone would be; the process exits with `status`.
118    WithStatus {
119        output: Box<Output<T>>,
120        status: ExitStatus,
121    },
122}
123impl<T: Serialize> Output<T> {
124    /// A signal beside the result, never a failure; a later call replaces the earlier status.
125    pub fn with_exit_status(self, status: ExitStatus) -> Self {
126        let (output, _) = self.split_exit_status();
127        Output::WithStatus {
128            output: Box::new(output),
129            status,
130        }
131    }
132    pub fn split_exit_status(self) -> (Self, Option<ExitStatus>) {
133        match self {
134            Output::WithStatus { output, status } => (output.split_exit_status().0, Some(status)),
135            other => (other, None),
136        }
137    }
138    pub fn exit_status(&self) -> ExitStatus {
139        match self {
140            Output::WithStatus { status, .. } => *status,
141            _ => ExitStatus::SUCCESS,
142        }
143    }
144    pub fn map_render(self, f: impl FnOnce(T) -> T) -> Self {
145        match self {
146            Output::Render(data) => Output::Render(f(data)),
147            Output::WithStatus { output, status } => Output::WithStatus {
148                output: Box::new(output.map_render(f)),
149                status,
150            },
151            other => other,
152        }
153    }
154    fn declared(&self) -> &Self {
155        match self {
156            Output::WithStatus { output, .. } => output.declared(),
157            other => other,
158        }
159    }
160    pub fn is_render(&self) -> bool {
161        matches!(self.declared(), Output::Render(_))
162    }
163    pub fn is_silent(&self) -> bool {
164        matches!(self.declared(), Output::Silent)
165    }
166    pub fn is_binary(&self) -> bool {
167        matches!(self.declared(), Output::Binary { .. })
168    }
169    pub fn is_artifact(&self) -> bool {
170        matches!(self.declared(), Output::Artifact(_))
171    }
172}
173pub type HandlerResult<T> = Result<Output<T>, anyhow::Error>;
174
175/// What a command that declares events returns once the events are done: they
176/// already carried the run's results, so a payload has nowhere left to go.
177#[derive(Debug)]
178#[non_exhaustive]
179pub enum Summary<T: Serialize> {
180    Render(T),
181    Silent,
182    /// Emitted as `summary` alone would be; the process exits with `status`.
183    WithStatus {
184        summary: Box<Summary<T>>,
185        status: ExitStatus,
186    },
187}
188
189impl<T: Serialize> Summary<T> {
190    /// A signal beside the result, never a failure; a later call replaces the earlier status.
191    pub fn with_exit_status(self, status: ExitStatus) -> Self {
192        let (summary, _) = self.split_exit_status();
193        Summary::WithStatus {
194            summary: Box::new(summary),
195            status,
196        }
197    }
198
199    pub fn split_exit_status(self) -> (Self, Option<ExitStatus>) {
200        match self {
201            Summary::WithStatus { summary, status } => {
202                (summary.split_exit_status().0, Some(status))
203            }
204            other => (other, None),
205        }
206    }
207
208    pub fn exit_status(&self) -> ExitStatus {
209        match self {
210            Summary::WithStatus { status, .. } => *status,
211            _ => ExitStatus::SUCCESS,
212        }
213    }
214
215    pub fn map_render(self, f: impl FnOnce(T) -> T) -> Self {
216        match self {
217            Summary::Render(data) => Summary::Render(f(data)),
218            Summary::WithStatus { summary, status } => Summary::WithStatus {
219                summary: Box::new(summary.map_render(f)),
220                status,
221            },
222            other => other,
223        }
224    }
225
226    fn declared(&self) -> &Self {
227        match self {
228            Summary::WithStatus { summary, .. } => summary.declared(),
229            other => other,
230        }
231    }
232
233    pub fn is_render(&self) -> bool {
234        matches!(self.declared(), Summary::Render(_))
235    }
236
237    pub fn is_silent(&self) -> bool {
238        matches!(self.declared(), Summary::Silent)
239    }
240}
241
242impl<T: Serialize> From<Summary<T>> for Output<T> {
243    fn from(summary: Summary<T>) -> Self {
244        match summary {
245            Summary::Render(data) => Output::Render(data),
246            Summary::Silent => Output::Silent,
247            Summary::WithStatus { summary, status } => Output::WithStatus {
248                output: Box::new(Output::from(*summary)),
249                status,
250            },
251        }
252    }
253}
254
255pub type SummaryResult<T> = Result<Summary<T>, anyhow::Error>;
256
257#[diagnostic::on_unimplemented(
258    message = "a command that declares events returns a `Summary`, not an `Output`",
259    note = "the events carried the run's results already, so a summary is `Render` or `Silent`"
260)]
261pub trait IntoSummaryResult<T: Serialize> {
262    fn into_summary_result(self) -> SummaryResult<T>;
263}
264
265#[diagnostic::do_not_recommend]
266impl<T, E> IntoSummaryResult<T> for Result<T, E>
267where
268    T: Serialize,
269    E: Into<anyhow::Error>,
270{
271    fn into_summary_result(self) -> SummaryResult<T> {
272        self.map(Summary::Render).map_err(Into::into)
273    }
274}
275
276impl<T, E> IntoSummaryResult<T> for Result<Summary<T>, E>
277where
278    T: Serialize,
279    E: Into<anyhow::Error>,
280{
281    fn into_summary_result(self) -> SummaryResult<T> {
282        self.map_err(Into::into)
283    }
284}
285
286mod outcome {
287    pub trait Sealed {}
288}
289
290/// What `Handler::handle` may return, tied to the command's event type:
291/// `Output<T>` is an outcome only for `NoEvents`, so a command that declares
292/// events has `Summary<T>` and no payload variant to return.
293#[diagnostic::on_unimplemented(
294    message = "a command that declares events returns `Summary<{T}>`, not `Output<{T}>`",
295    note = "the events carried the run's results already, so a summary is `Render` or `Silent`"
296)]
297pub trait HandlerOutcome<T: Serialize, E: Serialize + 'static>: outcome::Sealed {
298    fn into_output(self) -> Output<T>;
299}
300
301impl<T: Serialize> outcome::Sealed for Output<T> {}
302
303impl<T: Serialize> outcome::Sealed for Summary<T> {}
304
305impl<T: Serialize> HandlerOutcome<T, NoEvents> for Output<T> {
306    fn into_output(self) -> Output<T> {
307        self
308    }
309}
310
311impl<T: Serialize, E: Serialize + 'static> HandlerOutcome<T, E> for Summary<T> {
312    fn into_output(self) -> Output<T> {
313        Output::from(self)
314    }
315}
316
317pub trait IntoHandlerResult<T: Serialize> {
318    fn into_handler_result(self) -> HandlerResult<T>;
319}
320impl<T, E> IntoHandlerResult<T> for Result<T, E>
321where
322    T: Serialize,
323    E: Into<anyhow::Error>,
324{
325    fn into_handler_result(self) -> HandlerResult<T> {
326        self.map(Output::Render).map_err(Into::into)
327    }
328}
329impl<T: Serialize> IntoHandlerResult<T> for HandlerResult<T> {
330    fn into_handler_result(self) -> HandlerResult<T> {
331        self
332    }
333}
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
335pub struct ExitStatus(u8);
336impl ExitStatus {
337    pub const SUCCESS: Self = Self(0);
338    pub const FAILURE: Self = Self(1);
339    pub const USAGE_ERROR: Self = Self(2);
340    pub const fn code(self) -> u8 {
341        self.0
342    }
343}
344impl From<u8> for ExitStatus {
345    fn from(code: u8) -> Self {
346        Self(code)
347    }
348}
349#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
350#[error("an external failure status must be nonzero")]
351pub struct InvalidExternalStatus;
352#[derive(Debug, Clone)]
353pub struct ExternalFailure {
354    status: ExitStatus,
355    diagnostic: String,
356    source: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
357}
358impl ExternalFailure {
359    pub fn new(status: u8, diagnostic: impl Into<String>) -> Result<Self, InvalidExternalStatus> {
360        if status == 0 {
361            return Err(InvalidExternalStatus);
362        }
363        Ok(Self {
364            status: ExitStatus(status),
365            diagnostic: diagnostic.into(),
366            source: None,
367        })
368    }
369    pub const fn exit_status(&self) -> ExitStatus {
370        self.status
371    }
372    pub fn diagnostic(&self) -> &str {
373        &self.diagnostic
374    }
375    pub fn with_source<E>(mut self, source: E) -> Self
376    where
377        E: std::error::Error + Send + Sync + 'static,
378    {
379        self.source = Some(Arc::new(source));
380        self
381    }
382}
383impl fmt::Display for ExternalFailure {
384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385        f.write_str(self.diagnostic())
386    }
387}
388impl std::error::Error for ExternalFailure {
389    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
390        self.source
391            .as_deref()
392            .map(|source| source as &(dyn std::error::Error + 'static))
393    }
394}
395#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
396#[error("an app failure status must be nonzero")]
397pub struct InvalidAppStatus;
398#[derive(Debug, Clone)]
399pub struct AppFailure {
400    status: ExitStatus,
401    diagnostic: String,
402    source: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
403}
404impl AppFailure {
405    pub fn new(status: u8, diagnostic: impl Into<String>) -> Result<Self, InvalidAppStatus> {
406        if status == 0 {
407            return Err(InvalidAppStatus);
408        }
409        Ok(Self {
410            status: ExitStatus(status),
411            diagnostic: diagnostic.into(),
412            source: None,
413        })
414    }
415    pub const fn exit_status(&self) -> ExitStatus {
416        self.status
417    }
418    pub fn diagnostic(&self) -> &str {
419        &self.diagnostic
420    }
421    pub fn with_source<E>(mut self, source: E) -> Self
422    where
423        E: std::error::Error + Send + Sync + 'static,
424    {
425        self.source = Some(Arc::new(source));
426        self
427    }
428}
429impl fmt::Display for AppFailure {
430    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431        f.write_str(self.diagnostic())
432    }
433}
434impl std::error::Error for AppFailure {
435    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
436        self.source
437            .as_deref()
438            .map(|source| source as &(dyn std::error::Error + 'static))
439    }
440}
441#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
442#[non_exhaustive]
443pub enum SuccessKind {
444    Command,
445    ClapHelp,
446    ClapVersion,
447}
448#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
449#[non_exhaustive]
450pub enum OutputKind {
451    Text,
452    Binary,
453    Artifact,
454}
455#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
456#[non_exhaustive]
457pub enum RunErrorKind {
458    ClapUsage,
459    DefaultCommand,
460    Handler,
461    Hook(HookPhase),
462    Render,
463    FinalWrite(OutputKind),
464    External,
465    App,
466    Config,
467}
468#[derive(Debug, Clone)]
469pub struct RunOutput {
470    text: String,
471    kind: SuccessKind,
472    status: ExitStatus,
473    warnings_included: bool,
474}
475impl RunOutput {
476    pub fn command(text: impl Into<String>) -> Self {
477        Self::new(text, SuccessKind::Command)
478    }
479    pub fn clap_help(text: impl Into<String>) -> Self {
480        Self::new(text, SuccessKind::ClapHelp)
481    }
482    pub fn clap_version(text: impl Into<String>) -> Self {
483        Self::new(text, SuccessKind::ClapVersion)
484    }
485    fn new(text: impl Into<String>, kind: SuccessKind) -> Self {
486        Self {
487            text: text.into(),
488            kind,
489            status: ExitStatus::SUCCESS,
490            warnings_included: false,
491        }
492    }
493    pub fn with_exit_status(mut self, status: ExitStatus) -> Self {
494        self.status = status;
495        self
496    }
497    /// Marks output whose document already carries the run's warning records,
498    /// so the framework neither appends them nor renders them to stderr.
499    pub fn with_warnings_included(mut self, included: bool) -> Self {
500        self.warnings_included = included;
501        self
502    }
503    pub const fn warnings_included(&self) -> bool {
504        self.warnings_included
505    }
506    pub fn as_str(&self) -> &str {
507        &self.text
508    }
509    pub const fn kind(&self) -> SuccessKind {
510        self.kind
511    }
512    pub const fn exit_status(&self) -> ExitStatus {
513        self.status
514    }
515    pub fn into_string(self) -> String {
516        self.text
517    }
518}
519impl std::ops::Deref for RunOutput {
520    type Target = str;
521    fn deref(&self) -> &Self::Target {
522        self.as_str()
523    }
524}
525impl AsRef<str> for RunOutput {
526    fn as_ref(&self) -> &str {
527        self.as_str()
528    }
529}
530impl fmt::Display for RunOutput {
531    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
532        f.write_str(self.as_str())
533    }
534}
535impl PartialEq<str> for RunOutput {
536    fn eq(&self, other: &str) -> bool {
537        self.as_str() == other
538    }
539}
540impl PartialEq<&str> for RunOutput {
541    fn eq(&self, other: &&str) -> bool {
542        self.as_str() == *other
543    }
544}
545impl PartialEq<String> for RunOutput {
546    fn eq(&self, other: &String) -> bool {
547        self.as_str() == other
548    }
549}
550impl From<String> for RunOutput {
551    fn from(text: String) -> Self {
552        Self::command(text)
553    }
554}
555impl From<&str> for RunOutput {
556    fn from(text: &str) -> Self {
557        Self::command(text)
558    }
559}
560impl From<RunOutput> for String {
561    fn from(output: RunOutput) -> Self {
562        output.into_string()
563    }
564}
565#[derive(Debug, Clone)]
566pub struct RunError {
567    message: String,
568    kind: RunErrorKind,
569    status: ExitStatus,
570    source: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
571    diagnostic: Option<Box<Diagnostic>>,
572}
573impl RunError {
574    pub fn new(message: impl Into<String>, kind: RunErrorKind) -> Self {
575        assert!(
576            kind != RunErrorKind::External,
577            "external run errors must be constructed from ExternalFailure"
578        );
579        assert!(
580            kind != RunErrorKind::App,
581            "app run errors must be constructed from AppFailure"
582        );
583        let status = match kind {
584            RunErrorKind::ClapUsage => ExitStatus::USAGE_ERROR,
585            _ => ExitStatus::FAILURE,
586        };
587        Self {
588            message: message.into(),
589            kind,
590            status,
591            source: None,
592            diagnostic: None,
593        }
594    }
595    pub fn with_source<E>(mut self, source: E) -> Self
596    where
597        E: std::error::Error + Send + Sync + 'static,
598    {
599        self.source = Some(Arc::new(source));
600        self
601    }
602    /// Replaces the summary `diagnostic()` would otherwise derive from the prose message.
603    pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
604        self.diagnostic = Some(Box::new(diagnostic));
605        self
606    }
607    /// The carried diagnostic wins; otherwise the first prose line (one `Error: ` framing
608    /// stripped) is `summary` and the rest `detail`.
609    pub fn diagnostic(&self) -> Diagnostic {
610        let mut diagnostic = match (&self.diagnostic, self.kind) {
611            (Some(diagnostic), _) => (**diagnostic).clone(),
612            (None, RunErrorKind::External | RunErrorKind::App) => {
613                Diagnostic::error(first_line(&self.message)).detail(self.message.clone())
614            }
615            (None, _) => {
616                let prose = ["Error: ", "error: "]
617                    .iter()
618                    .find_map(|framing| self.message.strip_prefix(framing))
619                    .unwrap_or(&self.message);
620                let (summary, detail) = prose.split_once('\n').unwrap_or((prose, ""));
621                Diagnostic::error(summary.trim_end()).detail(detail.trim())
622            }
623        };
624        diagnostic.kind = self.kind.into();
625        diagnostic.severity = Severity::Error;
626        diagnostic
627    }
628    pub fn as_str(&self) -> &str {
629        &self.message
630    }
631    pub const fn kind(&self) -> RunErrorKind {
632        self.kind
633    }
634    pub const fn exit_status(&self) -> ExitStatus {
635        self.status
636    }
637    pub fn into_string(self) -> String {
638        self.message
639    }
640    // A stderr payload its owner wrote: no `Error: ` framing, no trailing newline.
641    pub const fn writes_diagnostic_verbatim(&self) -> bool {
642        matches!(self.kind, RunErrorKind::External | RunErrorKind::App)
643    }
644}
645impl std::ops::Deref for RunError {
646    type Target = str;
647    fn deref(&self) -> &Self::Target {
648        self.as_str()
649    }
650}
651impl AsRef<str> for RunError {
652    fn as_ref(&self) -> &str {
653        self.as_str()
654    }
655}
656impl fmt::Display for RunError {
657    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
658        f.write_str(self.as_str())
659    }
660}
661impl std::error::Error for RunError {
662    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
663        self.source
664            .as_deref()
665            .map(|source| source as &(dyn std::error::Error + 'static))
666    }
667}
668impl From<ExternalFailure> for RunError {
669    fn from(failure: ExternalFailure) -> Self {
670        Self {
671            message: failure.diagnostic,
672            kind: RunErrorKind::External,
673            status: failure.status,
674            source: failure.source,
675            diagnostic: None,
676        }
677    }
678}
679impl From<AppFailure> for RunError {
680    fn from(failure: AppFailure) -> Self {
681        Self {
682            message: failure.diagnostic,
683            kind: RunErrorKind::App,
684            status: failure.status,
685            source: failure.source,
686            diagnostic: None,
687        }
688    }
689}
690fn first_line(text: &str) -> &str {
691    text.lines().next().unwrap_or("").trim_end()
692}
693impl From<String> for RunError {
694    fn from(message: String) -> Self {
695        Self::new(message, RunErrorKind::Handler)
696    }
697}
698impl From<&str> for RunError {
699    fn from(message: &str) -> Self {
700        Self::new(message, RunErrorKind::Handler)
701    }
702}
703impl From<RunError> for String {
704    fn from(error: RunError) -> Self {
705        error.into_string()
706    }
707}
708#[derive(Debug)]
709#[non_exhaustive]
710pub enum DispatchResult {
711    Handled(RunOutput),
712    Binary(Vec<u8>, String),
713    Artifact(ArtifactRun),
714    Silent,
715    Error(RunError),
716    NoMatch(ArgMatches),
717}
718impl DispatchResult {
719    pub fn is_handled(&self) -> bool {
720        matches!(self, DispatchResult::Handled(_))
721    }
722    pub fn is_binary(&self) -> bool {
723        matches!(self, DispatchResult::Binary(_, _))
724    }
725    pub fn is_artifact(&self) -> bool {
726        matches!(self, DispatchResult::Artifact(_))
727    }
728    pub fn is_silent(&self) -> bool {
729        matches!(self, DispatchResult::Silent)
730    }
731    pub fn is_error(&self) -> bool {
732        matches!(self, DispatchResult::Error(_))
733    }
734    pub fn output(&self) -> Option<&str> {
735        match self {
736            DispatchResult::Handled(s) => Some(s),
737            _ => None,
738        }
739    }
740    pub fn error(&self) -> Option<&str> {
741        match self {
742            DispatchResult::Error(s) => Some(s),
743            _ => None,
744        }
745    }
746    pub fn success_kind(&self) -> Option<SuccessKind> {
747        match self {
748            DispatchResult::Handled(output) => Some(output.kind()),
749            DispatchResult::Binary(_, _) | DispatchResult::Artifact(_) | DispatchResult::Silent => {
750                Some(SuccessKind::Command)
751            }
752            _ => None,
753        }
754    }
755    pub fn error_kind(&self) -> Option<RunErrorKind> {
756        match self {
757            DispatchResult::Error(error) => Some(error.kind()),
758            _ => None,
759        }
760    }
761    pub fn exit_status(&self) -> Option<ExitStatus> {
762        match self {
763            DispatchResult::Handled(output) => Some(output.exit_status()),
764            DispatchResult::Binary(_, _) | DispatchResult::Artifact(_) | DispatchResult::Silent => {
765                Some(ExitStatus::SUCCESS)
766            }
767            DispatchResult::Error(error) => Some(error.exit_status()),
768            DispatchResult::NoMatch(_) => None,
769        }
770    }
771    pub fn binary(&self) -> Option<(&[u8], &str)> {
772        match self {
773            DispatchResult::Binary(bytes, filename) => Some((bytes, filename)),
774            _ => None,
775        }
776    }
777    pub fn artifact(&self) -> Option<&ArtifactRun> {
778        match self {
779            DispatchResult::Artifact(run) => Some(run),
780            _ => None,
781        }
782    }
783    pub fn matches(&self) -> Option<&ArgMatches> {
784        match self {
785            DispatchResult::NoMatch(m) => Some(m),
786            _ => None,
787        }
788    }
789}
790pub trait Handler {
791    /// The type of the values the command produces while it runs, or
792    /// [`NoEvents`] for a command that produces none, which is what
793    /// [`emits_events`](crate::emits_events) reads.
794    type Event: Serialize + 'static;
795    type Output: Serialize;
796    /// [`Output`] for a batch command, [`Summary`] for one that declares
797    /// events: [`HandlerOutcome`] admits the first only under [`NoEvents`].
798    type Outcome: HandlerOutcome<Self::Output, Self::Event>;
799    fn handle(
800        &mut self,
801        matches: &ArgMatches,
802        ctx: &CommandContext,
803        results: &mut Results<Self::Event>,
804    ) -> Result<Self::Outcome, anyhow::Error>;
805    fn expected_args(&self) -> Vec<ExpectedArg> {
806        Vec::new()
807    }
808}
809pub struct FnHandler<F, T, R = HandlerResult<T>>
810where
811    T: Serialize,
812{
813    f: F,
814    _phantom: std::marker::PhantomData<fn() -> (T, R)>,
815}
816impl<F, T, R> FnHandler<F, T, R>
817where
818    F: FnMut(&ArgMatches, &CommandContext) -> R,
819    R: IntoHandlerResult<T>,
820    T: Serialize,
821{
822    pub fn new(f: F) -> Self {
823        Self {
824            f,
825            _phantom: std::marker::PhantomData,
826        }
827    }
828}
829impl<F, T, R> Handler for FnHandler<F, T, R>
830where
831    F: FnMut(&ArgMatches, &CommandContext) -> R,
832    R: IntoHandlerResult<T>,
833    T: Serialize,
834{
835    type Event = NoEvents;
836    type Output = T;
837    type Outcome = Output<T>;
838    fn handle(
839        &mut self,
840        matches: &ArgMatches,
841        ctx: &CommandContext,
842        _results: &mut Results<NoEvents>,
843    ) -> HandlerResult<T> {
844        (self.f)(matches, ctx).into_handler_result()
845    }
846}
847/// The adapter behind a three-argument closure, whose third parameter is the
848/// command's typed results channel, so `Event` is the closure's event type.
849pub struct EventsFnHandler<F, E, T, R = SummaryResult<T>>
850where
851    E: Serialize + 'static,
852    T: Serialize,
853{
854    f: F,
855    _event: std::marker::PhantomData<fn(E)>,
856    _phantom: std::marker::PhantomData<fn() -> (T, R)>,
857}
858impl<F, E, T, R> EventsFnHandler<F, E, T, R>
859where
860    F: FnMut(&ArgMatches, &CommandContext, &mut Results<E>) -> R,
861    R: IntoSummaryResult<T>,
862    E: Serialize + 'static,
863    T: Serialize,
864{
865    pub fn new(f: F) -> Self {
866        Self {
867            f,
868            _event: std::marker::PhantomData,
869            _phantom: std::marker::PhantomData,
870        }
871    }
872}
873impl<F, E, T, R> Handler for EventsFnHandler<F, E, T, R>
874where
875    F: FnMut(&ArgMatches, &CommandContext, &mut Results<E>) -> R,
876    R: IntoSummaryResult<T>,
877    E: Serialize + 'static,
878    T: Serialize,
879{
880    type Event = E;
881    type Output = T;
882    type Outcome = Summary<T>;
883    fn handle(
884        &mut self,
885        matches: &ArgMatches,
886        ctx: &CommandContext,
887        results: &mut Results<E>,
888    ) -> SummaryResult<T> {
889        (self.f)(matches, ctx, results).into_summary_result()
890    }
891}
892pub struct SimpleFnHandler<F, T, R = HandlerResult<T>>
893where
894    T: Serialize,
895{
896    f: F,
897    _phantom: std::marker::PhantomData<fn() -> (T, R)>,
898}
899impl<F, T, R> SimpleFnHandler<F, T, R>
900where
901    F: FnMut(&ArgMatches) -> R,
902    R: IntoHandlerResult<T>,
903    T: Serialize,
904{
905    pub fn new(f: F) -> Self {
906        Self {
907            f,
908            _phantom: std::marker::PhantomData,
909        }
910    }
911}
912impl<F, T, R> Handler for SimpleFnHandler<F, T, R>
913where
914    F: FnMut(&ArgMatches) -> R,
915    R: IntoHandlerResult<T>,
916    T: Serialize,
917{
918    type Event = NoEvents;
919    type Output = T;
920    type Outcome = Output<T>;
921    fn handle(
922        &mut self,
923        matches: &ArgMatches,
924        _ctx: &CommandContext,
925        _results: &mut Results<NoEvents>,
926    ) -> HandlerResult<T> {
927        (self.f)(matches).into_handler_result()
928    }
929}
930#[cfg(test)]
931mod tests {
932    use super::*;
933    use crate::diagnostic::DiagnosticKind;
934    use serde_json::json;
935    #[test]
936    fn test_command_context_creation() {
937        let ctx = CommandContext {
938            command_path: vec!["config".into(), "get".into()],
939            app_state: Rc::new(Extensions::new()),
940            extensions: Extensions::new(),
941        };
942        assert_eq!(ctx.command_path, vec!["config", "get"]);
943    }
944    #[derive(Debug, thiserror::Error)]
945    #[error("the store refused")]
946    struct StoreRefused;
947
948    #[test]
949    fn a_summary_carrying_an_error_of_its_own_converts_to_a_summary_result() {
950        let rendered: Result<Summary<u8>, StoreRefused> = Ok(Summary::Render(7));
951        assert!(matches!(
952            rendered.into_summary_result(),
953            Ok(Summary::Render(7))
954        ));
955
956        let refused: Result<Summary<u8>, StoreRefused> = Err(StoreRefused);
957        assert_eq!(
958            refused.into_summary_result().unwrap_err().to_string(),
959            "the store refused"
960        );
961    }
962
963    #[test]
964    fn external_failure_rejects_success_and_preserves_metadata() {
965        assert_eq!(
966            ExternalFailure::new(0, "not a failure").unwrap_err(),
967            InvalidExternalStatus
968        );
969        let failure = ExternalFailure::new(128, "fatal: repository missing\n")
970            .unwrap()
971            .with_source(std::io::Error::other("git failed"));
972        assert_eq!(failure.exit_status().code(), 128);
973        assert_eq!(failure.diagnostic(), "fatal: repository missing\n");
974        assert_eq!(
975            std::error::Error::source(&failure).unwrap().to_string(),
976            "git failed"
977        );
978        let captured = RunError::from(failure);
979        assert_eq!(captured.kind(), RunErrorKind::External);
980        assert_eq!(captured.exit_status().code(), 128);
981        assert_eq!(captured.as_str(), "fatal: repository missing\n");
982        assert_eq!(
983            std::error::Error::source(&captured).unwrap().to_string(),
984            "git failed"
985        );
986    }
987    #[test]
988    #[should_panic(expected = "external run errors must be constructed from ExternalFailure")]
989    fn run_error_new_rejects_external_kind() {
990        let _ = RunError::new("inconsistent", RunErrorKind::External);
991    }
992    #[test]
993    fn app_failure_rejects_success_and_preserves_metadata() {
994        assert_eq!(
995            AppFailure::new(0, "not a failure").unwrap_err(),
996            InvalidAppStatus
997        );
998        let failure = AppFailure::new(1, "ghlike: repository not found: demo/gamma\n")
999            .unwrap()
1000            .with_source(std::io::Error::other("lookup failed"));
1001        assert_eq!(failure.exit_status().code(), 1);
1002        assert_eq!(
1003            failure.diagnostic(),
1004            "ghlike: repository not found: demo/gamma\n"
1005        );
1006        assert_eq!(
1007            std::error::Error::source(&failure).unwrap().to_string(),
1008            "lookup failed"
1009        );
1010        let captured = RunError::from(failure);
1011        assert_eq!(captured.kind(), RunErrorKind::App);
1012        assert_eq!(captured.exit_status().code(), 1);
1013        assert_eq!(
1014            captured.as_str(),
1015            "ghlike: repository not found: demo/gamma\n"
1016        );
1017        assert!(captured.writes_diagnostic_verbatim());
1018        assert_eq!(
1019            std::error::Error::source(&captured).unwrap().to_string(),
1020            "lookup failed"
1021        );
1022    }
1023    #[test]
1024    fn an_app_failure_can_never_report_shell_success() {
1025        assert!(AppFailure::new(0, "").is_err());
1026        for status in 1..=u8::MAX {
1027            let failure = AppFailure::new(status, "domain error").expect("nonzero is accepted");
1028            assert_ne!(failure.exit_status(), ExitStatus::SUCCESS);
1029            assert_ne!(RunError::from(failure).exit_status(), ExitStatus::SUCCESS);
1030        }
1031    }
1032    #[test]
1033    #[should_panic(expected = "app run errors must be constructed from AppFailure")]
1034    fn run_error_new_rejects_app_kind() {
1035        let _ = RunError::new("inconsistent", RunErrorKind::App);
1036    }
1037    #[test]
1038    fn test_command_context_default() {
1039        let ctx = CommandContext::default();
1040        assert!(ctx.command_path.is_empty());
1041        assert!(ctx.extensions.is_empty());
1042        assert!(ctx.app_state.is_empty());
1043    }
1044    #[test]
1045    fn test_command_context_with_app_state() {
1046        struct Database {
1047            url: String,
1048        }
1049        struct Config {
1050            debug: bool,
1051        }
1052        let mut app_state = Extensions::new();
1053        app_state.insert(Database {
1054            url: "postgres://localhost".into(),
1055        });
1056        app_state.insert(Config { debug: true });
1057        let app_state = Rc::new(app_state);
1058        let ctx = CommandContext {
1059            command_path: vec!["list".into()],
1060            app_state: app_state.clone(),
1061            extensions: Extensions::new(),
1062        };
1063        let db = ctx.app_state.get::<Database>().unwrap();
1064        assert_eq!(db.url, "postgres://localhost");
1065        let config = ctx.app_state.get::<Config>().unwrap();
1066        assert!(config.debug);
1067        assert_eq!(Rc::strong_count(&ctx.app_state), 2);
1068    }
1069    #[test]
1070    fn test_command_context_app_state_get_required() {
1071        struct Present;
1072        let mut app_state = Extensions::new();
1073        app_state.insert(Present);
1074        let ctx = CommandContext {
1075            command_path: vec![],
1076            app_state: Rc::new(app_state),
1077            extensions: Extensions::new(),
1078        };
1079        assert!(ctx.app_state.get_required::<Present>().is_ok());
1080        #[derive(Debug)]
1081        struct Missing;
1082        let err = ctx.app_state.get_required::<Missing>();
1083        assert!(err.is_err());
1084        assert!(err.unwrap_err().to_string().contains("Extension missing"));
1085    }
1086    #[test]
1087    fn test_extensions_insert_and_get() {
1088        struct MyState {
1089            value: i32,
1090        }
1091        let mut ext = Extensions::new();
1092        assert!(ext.is_empty());
1093        ext.insert(MyState { value: 42 });
1094        assert!(!ext.is_empty());
1095        assert_eq!(ext.len(), 1);
1096        let state = ext.get::<MyState>().unwrap();
1097        assert_eq!(state.value, 42);
1098    }
1099    #[test]
1100    fn test_extensions_get_mut() {
1101        struct Counter {
1102            count: i32,
1103        }
1104        let mut ext = Extensions::new();
1105        ext.insert(Counter { count: 0 });
1106        if let Some(counter) = ext.get_mut::<Counter>() {
1107            counter.count += 1;
1108        }
1109        assert_eq!(ext.get::<Counter>().unwrap().count, 1);
1110    }
1111    #[test]
1112    fn test_extensions_multiple_types() {
1113        struct TypeA(i32);
1114        struct TypeB(String);
1115        let mut ext = Extensions::new();
1116        ext.insert(TypeA(1));
1117        ext.insert(TypeB("hello".into()));
1118        assert_eq!(ext.len(), 2);
1119        assert_eq!(ext.get::<TypeA>().unwrap().0, 1);
1120        assert_eq!(ext.get::<TypeB>().unwrap().0, "hello");
1121    }
1122    #[test]
1123    fn test_extensions_replace() {
1124        struct Value(i32);
1125        let mut ext = Extensions::new();
1126        ext.insert(Value(1));
1127        let old = ext.insert(Value(2));
1128        assert_eq!(old.unwrap().0, 1);
1129        assert_eq!(ext.get::<Value>().unwrap().0, 2);
1130    }
1131    #[test]
1132    fn test_extensions_remove() {
1133        struct Value(i32);
1134        let mut ext = Extensions::new();
1135        ext.insert(Value(42));
1136        let removed = ext.remove::<Value>();
1137        assert_eq!(removed.unwrap().0, 42);
1138        assert!(ext.is_empty());
1139        assert!(ext.get::<Value>().is_none());
1140    }
1141    #[test]
1142    fn test_extensions_contains() {
1143        struct Present;
1144        struct Absent;
1145        let mut ext = Extensions::new();
1146        ext.insert(Present);
1147        assert!(ext.contains::<Present>());
1148        assert!(!ext.contains::<Absent>());
1149    }
1150    #[test]
1151    fn test_extensions_clear() {
1152        struct A;
1153        struct B;
1154        let mut ext = Extensions::new();
1155        ext.insert(A);
1156        ext.insert(B);
1157        assert_eq!(ext.len(), 2);
1158        ext.clear();
1159        assert!(ext.is_empty());
1160    }
1161    #[test]
1162    fn test_extensions_missing_type_returns_none() {
1163        struct NotInserted;
1164        let ext = Extensions::new();
1165        assert!(ext.get::<NotInserted>().is_none());
1166    }
1167    #[test]
1168    fn test_extensions_get_required() {
1169        #[derive(Debug)]
1170        struct Config {
1171            value: i32,
1172        }
1173        let mut ext = Extensions::new();
1174        ext.insert(Config { value: 100 });
1175        let val = ext.get_required::<Config>();
1176        assert!(val.is_ok());
1177        assert_eq!(val.unwrap().value, 100);
1178        #[derive(Debug)]
1179        struct Missing;
1180        let err = ext.get_required::<Missing>();
1181        assert!(err.is_err());
1182        assert!(err
1183            .unwrap_err()
1184            .to_string()
1185            .contains("Extension missing: type"));
1186    }
1187    #[test]
1188    fn test_extensions_get_mut_required() {
1189        #[derive(Debug)]
1190        struct State {
1191            count: i32,
1192        }
1193        let mut ext = Extensions::new();
1194        ext.insert(State { count: 0 });
1195        {
1196            let val = ext.get_mut_required::<State>();
1197            assert!(val.is_ok());
1198            val.unwrap().count += 1;
1199        }
1200        assert_eq!(ext.get_required::<State>().unwrap().count, 1);
1201        #[derive(Debug)]
1202        struct Missing;
1203        let err = ext.get_mut_required::<Missing>();
1204        assert!(err.is_err());
1205    }
1206    #[test]
1207    fn test_extensions_clone_behavior() {
1208        struct Data(#[allow(dead_code)] i32);
1209        let mut original = Extensions::new();
1210        original.insert(Data(42));
1211        let cloned = original.clone();
1212        assert!(original.get::<Data>().is_some());
1213        assert!(cloned.is_empty());
1214        assert!(cloned.get::<Data>().is_none());
1215    }
1216    #[test]
1217    fn test_output_render() {
1218        let output: Output<String> = Output::Render("success".into());
1219        assert!(output.is_render());
1220        assert!(!output.is_silent());
1221        assert!(!output.is_binary());
1222    }
1223    #[test]
1224    fn test_output_silent() {
1225        let output: Output<String> = Output::Silent;
1226        assert!(!output.is_render());
1227        assert!(output.is_silent());
1228        assert!(!output.is_binary());
1229    }
1230    #[test]
1231    fn a_declared_status_rides_beside_the_output_and_the_last_one_wins() {
1232        let plain: Output<String> = Output::Render("found nothing".into());
1233        assert_eq!(plain.exit_status(), ExitStatus::SUCCESS);
1234        assert_eq!(plain.split_exit_status().1, None);
1235
1236        let signalled = Output::Render(String::from("changes"))
1237            .with_exit_status(ExitStatus::from(3))
1238            .with_exit_status(ExitStatus::from(2));
1239        assert_eq!(signalled.exit_status(), ExitStatus::from(2));
1240        assert!(signalled.is_render());
1241        assert!(!signalled.is_silent());
1242
1243        let stamped = signalled.map_render(|text| format!("{text}!"));
1244        let (output, status) = stamped.split_exit_status();
1245        assert_eq!(status, Some(ExitStatus::from(2)));
1246        assert!(matches!(output, Output::Render(ref text) if text == "changes!"));
1247
1248        let silent: Output<()> = Output::Silent.with_exit_status(ExitStatus::from(4));
1249        assert!(silent.is_silent());
1250        assert_eq!(silent.split_exit_status().1, Some(ExitStatus::from(4)));
1251    }
1252    #[test]
1253    fn a_handled_run_reports_the_status_its_output_declared() {
1254        let handled = DispatchResult::Handled(
1255            RunOutput::command("plan").with_exit_status(ExitStatus::from(2)),
1256        );
1257        assert_eq!(handled.exit_status(), Some(ExitStatus::from(2)));
1258        assert_eq!(handled.success_kind(), Some(SuccessKind::Command));
1259        assert!(!handled.is_error());
1260        assert_eq!(
1261            DispatchResult::Handled(RunOutput::command("plan")).exit_status(),
1262            Some(ExitStatus::SUCCESS)
1263        );
1264    }
1265    #[test]
1266    fn test_output_binary() {
1267        let output: Output<String> = Output::Binary {
1268            data: vec![0x25, 0x50, 0x44, 0x46],
1269            filename: "report.pdf".into(),
1270        };
1271        assert!(!output.is_render());
1272        assert!(!output.is_silent());
1273        assert!(output.is_binary());
1274    }
1275    #[test]
1276    fn test_run_result_handled() {
1277        let result = DispatchResult::Handled("output".into());
1278        assert!(result.is_handled());
1279        assert!(!result.is_binary());
1280        assert!(!result.is_silent());
1281        assert_eq!(result.output(), Some("output"));
1282        assert!(result.matches().is_none());
1283    }
1284    #[test]
1285    fn test_run_result_silent() {
1286        let result = DispatchResult::Silent;
1287        assert!(!result.is_handled());
1288        assert!(!result.is_binary());
1289        assert!(result.is_silent());
1290    }
1291    #[test]
1292    fn test_run_result_binary() {
1293        let bytes = vec![0x25, 0x50, 0x44, 0x46];
1294        let result = DispatchResult::Binary(bytes.clone(), "report.pdf".into());
1295        assert!(!result.is_handled());
1296        assert!(result.is_binary());
1297        assert!(!result.is_silent());
1298        let (data, filename) = result.binary().unwrap();
1299        assert_eq!(data, &bytes);
1300        assert_eq!(filename, "report.pdf");
1301    }
1302    #[test]
1303    fn test_run_result_no_match() {
1304        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1305        let result = DispatchResult::NoMatch(matches);
1306        assert!(!result.is_handled());
1307        assert!(!result.is_binary());
1308        assert!(result.matches().is_some());
1309    }
1310    #[test]
1311    fn test_fn_handler() {
1312        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1313            Ok(Output::Render(json!({"status": "ok"})))
1314        });
1315        let ctx = CommandContext::default();
1316        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1317        let result = handler.handle(&matches, &ctx, &mut Results::discarding());
1318        assert!(result.is_ok());
1319    }
1320    #[test]
1321    fn test_fn_handler_mutation() {
1322        let mut counter = 0u32;
1323        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1324            counter += 1;
1325            Ok(Output::Render(counter))
1326        });
1327        let ctx = CommandContext::default();
1328        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1329        let _ = handler.handle(&matches, &ctx, &mut Results::discarding());
1330        let _ = handler.handle(&matches, &ctx, &mut Results::discarding());
1331        let result = handler.handle(&matches, &ctx, &mut Results::discarding());
1332        assert!(result.is_ok());
1333        if let Ok(Output::Render(count)) = result {
1334            assert_eq!(count, 3);
1335        }
1336    }
1337    #[test]
1338    fn test_into_handler_result_from_result_ok() {
1339        use super::IntoHandlerResult;
1340        let result: Result<String, anyhow::Error> = Ok("hello".to_string());
1341        let handler_result = result.into_handler_result();
1342        assert!(handler_result.is_ok());
1343        match handler_result.unwrap() {
1344            Output::Render(s) => assert_eq!(s, "hello"),
1345            _ => panic!("Expected Output::Render"),
1346        }
1347    }
1348    #[test]
1349    fn test_into_handler_result_from_result_err() {
1350        use super::IntoHandlerResult;
1351        let result: Result<String, anyhow::Error> = Err(anyhow::anyhow!("test error"));
1352        let handler_result = result.into_handler_result();
1353        assert!(handler_result.is_err());
1354        assert!(handler_result
1355            .unwrap_err()
1356            .to_string()
1357            .contains("test error"));
1358    }
1359    #[test]
1360    fn test_into_handler_result_passthrough_render() {
1361        use super::IntoHandlerResult;
1362        let handler_result: HandlerResult<String> = Ok(Output::Render("hello".to_string()));
1363        let result = handler_result.into_handler_result();
1364        assert!(result.is_ok());
1365        match result.unwrap() {
1366            Output::Render(s) => assert_eq!(s, "hello"),
1367            _ => panic!("Expected Output::Render"),
1368        }
1369    }
1370    #[test]
1371    fn test_into_handler_result_passthrough_silent() {
1372        use super::IntoHandlerResult;
1373        let handler_result: HandlerResult<String> = Ok(Output::Silent);
1374        let result = handler_result.into_handler_result();
1375        assert!(result.is_ok());
1376        assert!(matches!(result.unwrap(), Output::Silent));
1377    }
1378    #[test]
1379    fn test_into_handler_result_passthrough_binary() {
1380        use super::IntoHandlerResult;
1381        let handler_result: HandlerResult<String> = Ok(Output::Binary {
1382            data: vec![1, 2, 3],
1383            filename: "test.bin".to_string(),
1384        });
1385        let result = handler_result.into_handler_result();
1386        assert!(result.is_ok());
1387        match result.unwrap() {
1388            Output::Binary { data, filename } => {
1389                assert_eq!(data, vec![1, 2, 3]);
1390                assert_eq!(filename, "test.bin");
1391            }
1392            _ => panic!("Expected Output::Binary"),
1393        }
1394    }
1395    #[test]
1396    fn a_summary_becomes_the_output_the_presentation_pipeline_consumes() {
1397        assert!(Output::from(Summary::Render("done".to_string())).is_render());
1398        assert!(Output::from(Summary::<String>::Silent).is_silent());
1399
1400        let carried =
1401            Output::from(Summary::Render("done".to_string()).with_exit_status(ExitStatus::from(2)));
1402        assert_eq!(carried.exit_status(), ExitStatus::from(2));
1403        assert!(carried.is_render());
1404    }
1405
1406    #[test]
1407    fn a_later_exit_status_on_a_summary_replaces_the_earlier_one() {
1408        let summary = Summary::<String>::Silent
1409            .with_exit_status(ExitStatus::from(2))
1410            .with_exit_status(ExitStatus::from(3));
1411        let (declared, status) = summary.split_exit_status();
1412        assert_eq!(status, Some(ExitStatus::from(3)));
1413        assert!(declared.is_silent());
1414    }
1415
1416    #[test]
1417    fn a_plain_value_from_an_emitting_closure_becomes_a_rendered_summary() {
1418        use super::IntoSummaryResult;
1419        let summary = Ok::<_, anyhow::Error>("hello".to_string())
1420            .into_summary_result()
1421            .unwrap();
1422        assert!(matches!(summary, Summary::Render(ref s) if s == "hello"));
1423    }
1424
1425    #[test]
1426    fn test_fn_handler_with_auto_wrap() {
1427        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1428            Ok::<_, anyhow::Error>("auto-wrapped".to_string())
1429        });
1430        let ctx = CommandContext::default();
1431        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1432        let result = handler.handle(&matches, &ctx, &mut Results::discarding());
1433        assert!(result.is_ok());
1434        match result.unwrap() {
1435            Output::Render(s) => assert_eq!(s, "auto-wrapped"),
1436            _ => panic!("Expected Output::Render"),
1437        }
1438    }
1439    #[test]
1440    fn test_fn_handler_with_explicit_output() {
1441        let mut handler =
1442            FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| Ok(Output::<()>::Silent));
1443        let ctx = CommandContext::default();
1444        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1445        let result = handler.handle(&matches, &ctx, &mut Results::discarding());
1446        assert!(result.is_ok());
1447        assert!(matches!(result.unwrap(), Output::Silent));
1448    }
1449    #[test]
1450    fn test_fn_handler_with_custom_error_type() {
1451        #[derive(Debug)]
1452        struct CustomError(String);
1453        impl std::fmt::Display for CustomError {
1454            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1455                write!(f, "CustomError: {}", self.0)
1456            }
1457        }
1458        impl std::error::Error for CustomError {}
1459        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1460            Err::<String, CustomError>(CustomError("oops".to_string()))
1461        });
1462        let ctx = CommandContext::default();
1463        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1464        let result = handler.handle(&matches, &ctx, &mut Results::discarding());
1465        assert!(result.is_err());
1466        assert!(result
1467            .unwrap_err()
1468            .to_string()
1469            .contains("CustomError: oops"));
1470    }
1471    #[test]
1472    fn test_simple_fn_handler_basic() {
1473        use super::SimpleFnHandler;
1474        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1475            Ok::<_, anyhow::Error>("no context needed".to_string())
1476        });
1477        let ctx = CommandContext::default();
1478        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1479        let result = handler.handle(&matches, &ctx, &mut Results::discarding());
1480        assert!(result.is_ok());
1481        match result.unwrap() {
1482            Output::Render(s) => assert_eq!(s, "no context needed"),
1483            _ => panic!("Expected Output::Render"),
1484        }
1485    }
1486    #[test]
1487    fn test_simple_fn_handler_with_args() {
1488        use super::SimpleFnHandler;
1489        let mut handler = SimpleFnHandler::new(|m: &ArgMatches| {
1490            let verbose = m.get_flag("verbose");
1491            Ok::<_, anyhow::Error>(verbose)
1492        });
1493        let ctx = CommandContext::default();
1494        let matches = clap::Command::new("test")
1495            .arg(
1496                clap::Arg::new("verbose")
1497                    .short('v')
1498                    .action(clap::ArgAction::SetTrue),
1499            )
1500            .get_matches_from(vec!["test", "-v"]);
1501        let result = handler.handle(&matches, &ctx, &mut Results::discarding());
1502        assert!(result.is_ok());
1503        match result.unwrap() {
1504            Output::Render(v) => assert!(v),
1505            _ => panic!("Expected Output::Render"),
1506        }
1507    }
1508    #[test]
1509    fn test_simple_fn_handler_explicit_output() {
1510        use super::SimpleFnHandler;
1511        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| Ok(Output::<()>::Silent));
1512        let ctx = CommandContext::default();
1513        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1514        let result = handler.handle(&matches, &ctx, &mut Results::discarding());
1515        assert!(result.is_ok());
1516        assert!(matches!(result.unwrap(), Output::Silent));
1517    }
1518    #[test]
1519    fn test_simple_fn_handler_error() {
1520        use super::SimpleFnHandler;
1521        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1522            Err::<String, _>(anyhow::anyhow!("simple error"))
1523        });
1524        let ctx = CommandContext::default();
1525        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1526        let result = handler.handle(&matches, &ctx, &mut Results::discarding());
1527        assert!(result.is_err());
1528        assert!(result.unwrap_err().to_string().contains("simple error"));
1529    }
1530    #[test]
1531    fn test_simple_fn_handler_mutation() {
1532        use super::SimpleFnHandler;
1533        let mut counter = 0u32;
1534        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1535            counter += 1;
1536            Ok::<_, anyhow::Error>(counter)
1537        });
1538        let ctx = CommandContext::default();
1539        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1540        let _ = handler.handle(&matches, &ctx, &mut Results::discarding());
1541        let _ = handler.handle(&matches, &ctx, &mut Results::discarding());
1542        let result = handler.handle(&matches, &ctx, &mut Results::discarding());
1543        assert!(result.is_ok());
1544        match result.unwrap() {
1545            Output::Render(n) => assert_eq!(n, 3),
1546            _ => panic!("Expected Output::Render"),
1547        }
1548    }
1549
1550    #[test]
1551    fn a_carried_diagnostic_wins_and_takes_the_framework_kind() {
1552        let carried = Diagnostic::error("line 2 does not parse")
1553            .detail("expected `resource <name> <state>`")
1554            .range("main.tfl", 2, 1);
1555        let error = RunError::new("Error: line 2 does not parse", RunErrorKind::Handler)
1556            .with_diagnostic(carried.clone());
1557        let diagnostic = error.diagnostic();
1558        assert_eq!(diagnostic.kind, DiagnosticKind::Handler);
1559        assert_eq!(diagnostic.severity, Severity::Error);
1560        assert_eq!(diagnostic.summary, carried.summary);
1561        assert_eq!(diagnostic.detail, carried.detail);
1562        assert_eq!(diagnostic.range, carried.range);
1563        let mut hook_carried = Diagnostic::warning("soft");
1564        hook_carried.kind = DiagnosticKind::ClapUsage;
1565        let hook = RunError::new("Error: soft", RunErrorKind::Hook(HookPhase::PostDispatch))
1566            .with_diagnostic(hook_carried);
1567        let hook = hook.diagnostic();
1568        assert_eq!(hook.kind, DiagnosticKind::HookPostDispatch);
1569        assert_eq!(hook.severity, Severity::Error);
1570    }
1571    #[test]
1572    fn a_prose_error_splits_into_summary_and_detail_without_its_framing() {
1573        let clap = RunError::new(
1574            "error: unexpected argument '--bogus' found\n\nUsage: app [OPTIONS]\n\nFor more information, try '--help'.\n",
1575            RunErrorKind::ClapUsage,
1576        )
1577        .diagnostic();
1578        assert_eq!(clap.kind, DiagnosticKind::ClapUsage);
1579        assert_eq!(clap.summary, "unexpected argument '--bogus' found");
1580        assert_eq!(
1581            clap.detail,
1582            "Usage: app [OPTIONS]\n\nFor more information, try '--help'."
1583        );
1584        assert_eq!(clap.range, None);
1585        let framed =
1586            RunError::new("Error: could not read config", RunErrorKind::Render).diagnostic();
1587        assert_eq!(framed.summary, "could not read config");
1588        assert_eq!(framed.detail, "");
1589        let bare = RunError::new("plain", RunErrorKind::FinalWrite(OutputKind::Text)).diagnostic();
1590        assert_eq!(bare.summary, "plain");
1591        assert_eq!(bare.kind, DiagnosticKind::FinalWrite);
1592    }
1593    #[test]
1594    fn owner_declared_failures_keep_their_bytes_as_detail() {
1595        let app = RunError::from(
1596            AppFailure::new(3, "ghlike: not found: demo/gamma\nsee --help\n").unwrap(),
1597        )
1598        .diagnostic();
1599        assert_eq!(app.kind, DiagnosticKind::App);
1600        assert_eq!(app.summary, "ghlike: not found: demo/gamma");
1601        assert_eq!(app.detail, "ghlike: not found: demo/gamma\nsee --help\n");
1602        let external = RunError::from(ExternalFailure::new(128, "").unwrap()).diagnostic();
1603        assert_eq!(external.kind, DiagnosticKind::External);
1604        assert_eq!(external.summary, "");
1605        assert_eq!(external.detail, "");
1606    }
1607}