Skip to main content

standout_dispatch/
handler.rs

1use crate::artifact::{Artifact, ArtifactRun};
2use crate::diagnostic::{Diagnostic, Severity};
3use crate::hooks::HookPhase;
4use crate::stream::EntryStream;
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    pub stream: EntryStream,
89}
90impl CommandContext {
91    pub fn new(command_path: Vec<String>, app_state: Rc<Extensions>) -> Self {
92        Self {
93            command_path,
94            app_state,
95            extensions: Extensions::new(),
96            stream: EntryStream::discarding(),
97        }
98    }
99    pub fn with_stream(mut self, stream: EntryStream) -> Self {
100        self.stream = stream;
101        self
102    }
103    /// Live under `ndjson`, discarding in every other mode.
104    pub fn stream(&self) -> &EntryStream {
105        &self.stream
106    }
107}
108impl Default for CommandContext {
109    fn default() -> Self {
110        Self {
111            command_path: Vec::new(),
112            app_state: Rc::new(Extensions::new()),
113            extensions: Extensions::new(),
114            stream: EntryStream::discarding(),
115        }
116    }
117}
118#[derive(Debug)]
119#[non_exhaustive]
120pub enum Output<T: Serialize> {
121    Render(T),
122    Silent,
123    Binary {
124        data: Vec<u8>,
125        filename: String,
126    },
127    Artifact(Artifact<T>),
128    /// Emitted as `output` alone would be; the process exits with `status`.
129    WithStatus {
130        output: Box<Output<T>>,
131        status: ExitStatus,
132    },
133}
134impl<T: Serialize> Output<T> {
135    /// A signal beside the result, never a failure; a later call replaces the earlier status.
136    pub fn with_exit_status(self, status: ExitStatus) -> Self {
137        let (output, _) = self.split_exit_status();
138        Output::WithStatus {
139            output: Box::new(output),
140            status,
141        }
142    }
143    pub fn split_exit_status(self) -> (Self, Option<ExitStatus>) {
144        match self {
145            Output::WithStatus { output, status } => (output.split_exit_status().0, Some(status)),
146            other => (other, None),
147        }
148    }
149    pub fn exit_status(&self) -> ExitStatus {
150        match self {
151            Output::WithStatus { status, .. } => *status,
152            _ => ExitStatus::SUCCESS,
153        }
154    }
155    pub fn map_render(self, f: impl FnOnce(T) -> T) -> Self {
156        match self {
157            Output::Render(data) => Output::Render(f(data)),
158            Output::WithStatus { output, status } => Output::WithStatus {
159                output: Box::new(output.map_render(f)),
160                status,
161            },
162            other => other,
163        }
164    }
165    fn declared(&self) -> &Self {
166        match self {
167            Output::WithStatus { output, .. } => output.declared(),
168            other => other,
169        }
170    }
171    pub fn is_render(&self) -> bool {
172        matches!(self.declared(), Output::Render(_))
173    }
174    pub fn is_silent(&self) -> bool {
175        matches!(self.declared(), Output::Silent)
176    }
177    pub fn is_binary(&self) -> bool {
178        matches!(self.declared(), Output::Binary { .. })
179    }
180    pub fn is_artifact(&self) -> bool {
181        matches!(self.declared(), Output::Artifact(_))
182    }
183}
184pub type HandlerResult<T> = Result<Output<T>, anyhow::Error>;
185pub trait IntoHandlerResult<T: Serialize> {
186    fn into_handler_result(self) -> HandlerResult<T>;
187}
188impl<T, E> IntoHandlerResult<T> for Result<T, E>
189where
190    T: Serialize,
191    E: Into<anyhow::Error>,
192{
193    fn into_handler_result(self) -> HandlerResult<T> {
194        self.map(Output::Render).map_err(Into::into)
195    }
196}
197impl<T: Serialize> IntoHandlerResult<T> for HandlerResult<T> {
198    fn into_handler_result(self) -> HandlerResult<T> {
199        self
200    }
201}
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
203pub struct ExitStatus(u8);
204impl ExitStatus {
205    pub const SUCCESS: Self = Self(0);
206    pub const FAILURE: Self = Self(1);
207    pub const USAGE_ERROR: Self = Self(2);
208    pub const fn code(self) -> u8 {
209        self.0
210    }
211}
212impl From<u8> for ExitStatus {
213    fn from(code: u8) -> Self {
214        Self(code)
215    }
216}
217#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
218#[error("an external failure status must be nonzero")]
219pub struct InvalidExternalStatus;
220#[derive(Debug, Clone)]
221pub struct ExternalFailure {
222    status: ExitStatus,
223    diagnostic: String,
224    source: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
225}
226impl ExternalFailure {
227    pub fn new(status: u8, diagnostic: impl Into<String>) -> Result<Self, InvalidExternalStatus> {
228        if status == 0 {
229            return Err(InvalidExternalStatus);
230        }
231        Ok(Self {
232            status: ExitStatus(status),
233            diagnostic: diagnostic.into(),
234            source: None,
235        })
236    }
237    pub const fn exit_status(&self) -> ExitStatus {
238        self.status
239    }
240    pub fn diagnostic(&self) -> &str {
241        &self.diagnostic
242    }
243    pub fn with_source<E>(mut self, source: E) -> Self
244    where
245        E: std::error::Error + Send + Sync + 'static,
246    {
247        self.source = Some(Arc::new(source));
248        self
249    }
250}
251impl fmt::Display for ExternalFailure {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        f.write_str(self.diagnostic())
254    }
255}
256impl std::error::Error for ExternalFailure {
257    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
258        self.source
259            .as_deref()
260            .map(|source| source as &(dyn std::error::Error + 'static))
261    }
262}
263#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
264#[error("an app failure status must be nonzero")]
265pub struct InvalidAppStatus;
266#[derive(Debug, Clone)]
267pub struct AppFailure {
268    status: ExitStatus,
269    diagnostic: String,
270    source: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
271}
272impl AppFailure {
273    pub fn new(status: u8, diagnostic: impl Into<String>) -> Result<Self, InvalidAppStatus> {
274        if status == 0 {
275            return Err(InvalidAppStatus);
276        }
277        Ok(Self {
278            status: ExitStatus(status),
279            diagnostic: diagnostic.into(),
280            source: None,
281        })
282    }
283    pub const fn exit_status(&self) -> ExitStatus {
284        self.status
285    }
286    pub fn diagnostic(&self) -> &str {
287        &self.diagnostic
288    }
289    pub fn with_source<E>(mut self, source: E) -> Self
290    where
291        E: std::error::Error + Send + Sync + 'static,
292    {
293        self.source = Some(Arc::new(source));
294        self
295    }
296}
297impl fmt::Display for AppFailure {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        f.write_str(self.diagnostic())
300    }
301}
302impl std::error::Error for AppFailure {
303    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
304        self.source
305            .as_deref()
306            .map(|source| source as &(dyn std::error::Error + 'static))
307    }
308}
309#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
310#[non_exhaustive]
311pub enum SuccessKind {
312    Command,
313    ClapHelp,
314    ClapVersion,
315    PagedHelp,
316}
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
318#[non_exhaustive]
319pub enum OutputKind {
320    Text,
321    Binary,
322    Artifact,
323}
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
325#[non_exhaustive]
326pub enum RunErrorKind {
327    ClapUsage,
328    DefaultCommand,
329    Handler,
330    Hook(HookPhase),
331    Render,
332    FinalWrite(OutputKind),
333    External,
334    App,
335}
336#[derive(Debug, Clone)]
337pub struct RunOutput {
338    text: String,
339    kind: SuccessKind,
340    status: ExitStatus,
341}
342impl RunOutput {
343    pub fn command(text: impl Into<String>) -> Self {
344        Self::new(text, SuccessKind::Command)
345    }
346    pub fn clap_help(text: impl Into<String>) -> Self {
347        Self::new(text, SuccessKind::ClapHelp)
348    }
349    pub fn paged_help(text: impl Into<String>) -> Self {
350        Self::new(text, SuccessKind::PagedHelp)
351    }
352    pub fn clap_version(text: impl Into<String>) -> Self {
353        Self::new(text, SuccessKind::ClapVersion)
354    }
355    fn new(text: impl Into<String>, kind: SuccessKind) -> Self {
356        Self {
357            text: text.into(),
358            kind,
359            status: ExitStatus::SUCCESS,
360        }
361    }
362    pub fn with_exit_status(mut self, status: ExitStatus) -> Self {
363        self.status = status;
364        self
365    }
366    pub fn as_str(&self) -> &str {
367        &self.text
368    }
369    pub const fn kind(&self) -> SuccessKind {
370        self.kind
371    }
372    pub const fn exit_status(&self) -> ExitStatus {
373        self.status
374    }
375    pub fn into_string(self) -> String {
376        self.text
377    }
378}
379impl std::ops::Deref for RunOutput {
380    type Target = str;
381    fn deref(&self) -> &Self::Target {
382        self.as_str()
383    }
384}
385impl AsRef<str> for RunOutput {
386    fn as_ref(&self) -> &str {
387        self.as_str()
388    }
389}
390impl fmt::Display for RunOutput {
391    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392        f.write_str(self.as_str())
393    }
394}
395impl PartialEq<str> for RunOutput {
396    fn eq(&self, other: &str) -> bool {
397        self.as_str() == other
398    }
399}
400impl PartialEq<&str> for RunOutput {
401    fn eq(&self, other: &&str) -> bool {
402        self.as_str() == *other
403    }
404}
405impl PartialEq<String> for RunOutput {
406    fn eq(&self, other: &String) -> bool {
407        self.as_str() == other
408    }
409}
410impl From<String> for RunOutput {
411    fn from(text: String) -> Self {
412        Self::command(text)
413    }
414}
415impl From<&str> for RunOutput {
416    fn from(text: &str) -> Self {
417        Self::command(text)
418    }
419}
420impl From<RunOutput> for String {
421    fn from(output: RunOutput) -> Self {
422        output.into_string()
423    }
424}
425#[derive(Debug, Clone)]
426pub struct RunError {
427    message: String,
428    kind: RunErrorKind,
429    status: ExitStatus,
430    source: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
431    diagnostic: Option<Box<Diagnostic>>,
432}
433impl RunError {
434    pub fn new(message: impl Into<String>, kind: RunErrorKind) -> Self {
435        assert!(
436            kind != RunErrorKind::External,
437            "external run errors must be constructed from ExternalFailure"
438        );
439        assert!(
440            kind != RunErrorKind::App,
441            "app run errors must be constructed from AppFailure"
442        );
443        let status = match kind {
444            RunErrorKind::ClapUsage => ExitStatus::USAGE_ERROR,
445            _ => ExitStatus::FAILURE,
446        };
447        Self {
448            message: message.into(),
449            kind,
450            status,
451            source: None,
452            diagnostic: None,
453        }
454    }
455    pub fn with_source<E>(mut self, source: E) -> Self
456    where
457        E: std::error::Error + Send + Sync + 'static,
458    {
459        self.source = Some(Arc::new(source));
460        self
461    }
462    /// Replaces the summary `diagnostic()` would otherwise derive from the prose message.
463    pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
464        self.diagnostic = Some(Box::new(diagnostic));
465        self
466    }
467    /// The carried diagnostic wins; otherwise the first prose line (one `Error: ` framing
468    /// stripped) is `summary` and the rest `detail`.
469    pub fn diagnostic(&self) -> Diagnostic {
470        let mut diagnostic = match (&self.diagnostic, self.kind) {
471            (Some(diagnostic), _) => (**diagnostic).clone(),
472            (None, RunErrorKind::External | RunErrorKind::App) => {
473                Diagnostic::error(first_line(&self.message)).detail(self.message.clone())
474            }
475            (None, _) => {
476                let prose = ["Error: ", "error: "]
477                    .iter()
478                    .find_map(|framing| self.message.strip_prefix(framing))
479                    .unwrap_or(&self.message);
480                let (summary, detail) = prose.split_once('\n').unwrap_or((prose, ""));
481                Diagnostic::error(summary.trim_end()).detail(detail.trim())
482            }
483        };
484        diagnostic.kind = self.kind.into();
485        diagnostic.severity = Severity::Error;
486        diagnostic
487    }
488    pub fn as_str(&self) -> &str {
489        &self.message
490    }
491    pub const fn kind(&self) -> RunErrorKind {
492        self.kind
493    }
494    pub const fn exit_status(&self) -> ExitStatus {
495        self.status
496    }
497    pub fn into_string(self) -> String {
498        self.message
499    }
500    // A stderr payload its owner wrote: no `Error: ` framing, no trailing newline.
501    pub const fn writes_diagnostic_verbatim(&self) -> bool {
502        matches!(self.kind, RunErrorKind::External | RunErrorKind::App)
503    }
504}
505impl std::ops::Deref for RunError {
506    type Target = str;
507    fn deref(&self) -> &Self::Target {
508        self.as_str()
509    }
510}
511impl AsRef<str> for RunError {
512    fn as_ref(&self) -> &str {
513        self.as_str()
514    }
515}
516impl fmt::Display for RunError {
517    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518        f.write_str(self.as_str())
519    }
520}
521impl std::error::Error for RunError {
522    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
523        self.source
524            .as_deref()
525            .map(|source| source as &(dyn std::error::Error + 'static))
526    }
527}
528impl From<ExternalFailure> for RunError {
529    fn from(failure: ExternalFailure) -> Self {
530        Self {
531            message: failure.diagnostic,
532            kind: RunErrorKind::External,
533            status: failure.status,
534            source: failure.source,
535            diagnostic: None,
536        }
537    }
538}
539impl From<AppFailure> for RunError {
540    fn from(failure: AppFailure) -> Self {
541        Self {
542            message: failure.diagnostic,
543            kind: RunErrorKind::App,
544            status: failure.status,
545            source: failure.source,
546            diagnostic: None,
547        }
548    }
549}
550fn first_line(text: &str) -> &str {
551    text.lines().next().unwrap_or("").trim_end()
552}
553impl From<String> for RunError {
554    fn from(message: String) -> Self {
555        Self::new(message, RunErrorKind::Handler)
556    }
557}
558impl From<&str> for RunError {
559    fn from(message: &str) -> Self {
560        Self::new(message, RunErrorKind::Handler)
561    }
562}
563impl From<RunError> for String {
564    fn from(error: RunError) -> Self {
565        error.into_string()
566    }
567}
568#[derive(Debug)]
569#[non_exhaustive]
570pub enum DispatchResult {
571    Handled(RunOutput),
572    Binary(Vec<u8>, String),
573    Artifact(ArtifactRun),
574    Silent,
575    Error(RunError),
576    NoMatch(ArgMatches),
577}
578impl DispatchResult {
579    pub fn is_handled(&self) -> bool {
580        matches!(self, DispatchResult::Handled(_))
581    }
582    pub fn is_binary(&self) -> bool {
583        matches!(self, DispatchResult::Binary(_, _))
584    }
585    pub fn is_artifact(&self) -> bool {
586        matches!(self, DispatchResult::Artifact(_))
587    }
588    pub fn is_silent(&self) -> bool {
589        matches!(self, DispatchResult::Silent)
590    }
591    pub fn is_error(&self) -> bool {
592        matches!(self, DispatchResult::Error(_))
593    }
594    pub fn output(&self) -> Option<&str> {
595        match self {
596            DispatchResult::Handled(s) => Some(s),
597            _ => None,
598        }
599    }
600    pub fn error(&self) -> Option<&str> {
601        match self {
602            DispatchResult::Error(s) => Some(s),
603            _ => None,
604        }
605    }
606    pub fn success_kind(&self) -> Option<SuccessKind> {
607        match self {
608            DispatchResult::Handled(output) => Some(output.kind()),
609            DispatchResult::Binary(_, _) | DispatchResult::Artifact(_) | DispatchResult::Silent => {
610                Some(SuccessKind::Command)
611            }
612            _ => None,
613        }
614    }
615    pub fn error_kind(&self) -> Option<RunErrorKind> {
616        match self {
617            DispatchResult::Error(error) => Some(error.kind()),
618            _ => None,
619        }
620    }
621    pub fn exit_status(&self) -> Option<ExitStatus> {
622        match self {
623            DispatchResult::Handled(output) => Some(output.exit_status()),
624            DispatchResult::Binary(_, _) | DispatchResult::Artifact(_) | DispatchResult::Silent => {
625                Some(ExitStatus::SUCCESS)
626            }
627            DispatchResult::Error(error) => Some(error.exit_status()),
628            DispatchResult::NoMatch(_) => None,
629        }
630    }
631    pub fn binary(&self) -> Option<(&[u8], &str)> {
632        match self {
633            DispatchResult::Binary(bytes, filename) => Some((bytes, filename)),
634            _ => None,
635        }
636    }
637    pub fn artifact(&self) -> Option<&ArtifactRun> {
638        match self {
639            DispatchResult::Artifact(run) => Some(run),
640            _ => None,
641        }
642    }
643    pub fn matches(&self) -> Option<&ArgMatches> {
644        match self {
645            DispatchResult::NoMatch(m) => Some(m),
646            _ => None,
647        }
648    }
649}
650pub trait Handler {
651    type Output: Serialize;
652    fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext)
653        -> HandlerResult<Self::Output>;
654    fn expected_args(&self) -> Vec<ExpectedArg> {
655        Vec::new()
656    }
657}
658pub struct FnHandler<F, T, R = HandlerResult<T>>
659where
660    T: Serialize,
661{
662    f: F,
663    _phantom: std::marker::PhantomData<fn() -> (T, R)>,
664}
665impl<F, T, R> FnHandler<F, T, R>
666where
667    F: FnMut(&ArgMatches, &CommandContext) -> R,
668    R: IntoHandlerResult<T>,
669    T: Serialize,
670{
671    pub fn new(f: F) -> Self {
672        Self {
673            f,
674            _phantom: std::marker::PhantomData,
675        }
676    }
677}
678impl<F, T, R> Handler for FnHandler<F, T, R>
679where
680    F: FnMut(&ArgMatches, &CommandContext) -> R,
681    R: IntoHandlerResult<T>,
682    T: Serialize,
683{
684    type Output = T;
685    fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<T> {
686        (self.f)(matches, ctx).into_handler_result()
687    }
688}
689pub struct SimpleFnHandler<F, T, R = HandlerResult<T>>
690where
691    T: Serialize,
692{
693    f: F,
694    _phantom: std::marker::PhantomData<fn() -> (T, R)>,
695}
696impl<F, T, R> SimpleFnHandler<F, T, R>
697where
698    F: FnMut(&ArgMatches) -> R,
699    R: IntoHandlerResult<T>,
700    T: Serialize,
701{
702    pub fn new(f: F) -> Self {
703        Self {
704            f,
705            _phantom: std::marker::PhantomData,
706        }
707    }
708}
709impl<F, T, R> Handler for SimpleFnHandler<F, T, R>
710where
711    F: FnMut(&ArgMatches) -> R,
712    R: IntoHandlerResult<T>,
713    T: Serialize,
714{
715    type Output = T;
716    fn handle(&mut self, matches: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<T> {
717        (self.f)(matches).into_handler_result()
718    }
719}
720#[cfg(test)]
721mod tests {
722    use super::*;
723    use crate::diagnostic::DiagnosticKind;
724    use serde_json::json;
725    #[test]
726    fn test_command_context_creation() {
727        let ctx = CommandContext {
728            command_path: vec!["config".into(), "get".into()],
729            app_state: Rc::new(Extensions::new()),
730            extensions: Extensions::new(),
731            stream: EntryStream::discarding(),
732        };
733        assert_eq!(ctx.command_path, vec!["config", "get"]);
734    }
735    #[test]
736    fn external_failure_rejects_success_and_preserves_metadata() {
737        assert_eq!(
738            ExternalFailure::new(0, "not a failure").unwrap_err(),
739            InvalidExternalStatus
740        );
741        let failure = ExternalFailure::new(128, "fatal: repository missing\n")
742            .unwrap()
743            .with_source(std::io::Error::other("git failed"));
744        assert_eq!(failure.exit_status().code(), 128);
745        assert_eq!(failure.diagnostic(), "fatal: repository missing\n");
746        assert_eq!(
747            std::error::Error::source(&failure).unwrap().to_string(),
748            "git failed"
749        );
750        let captured = RunError::from(failure);
751        assert_eq!(captured.kind(), RunErrorKind::External);
752        assert_eq!(captured.exit_status().code(), 128);
753        assert_eq!(captured.as_str(), "fatal: repository missing\n");
754        assert_eq!(
755            std::error::Error::source(&captured).unwrap().to_string(),
756            "git failed"
757        );
758    }
759    #[test]
760    #[should_panic(expected = "external run errors must be constructed from ExternalFailure")]
761    fn run_error_new_rejects_external_kind() {
762        let _ = RunError::new("inconsistent", RunErrorKind::External);
763    }
764    #[test]
765    fn app_failure_rejects_success_and_preserves_metadata() {
766        assert_eq!(
767            AppFailure::new(0, "not a failure").unwrap_err(),
768            InvalidAppStatus
769        );
770        let failure = AppFailure::new(1, "ghlike: repository not found: demo/gamma\n")
771            .unwrap()
772            .with_source(std::io::Error::other("lookup failed"));
773        assert_eq!(failure.exit_status().code(), 1);
774        assert_eq!(
775            failure.diagnostic(),
776            "ghlike: repository not found: demo/gamma\n"
777        );
778        assert_eq!(
779            std::error::Error::source(&failure).unwrap().to_string(),
780            "lookup failed"
781        );
782        let captured = RunError::from(failure);
783        assert_eq!(captured.kind(), RunErrorKind::App);
784        assert_eq!(captured.exit_status().code(), 1);
785        assert_eq!(
786            captured.as_str(),
787            "ghlike: repository not found: demo/gamma\n"
788        );
789        assert!(captured.writes_diagnostic_verbatim());
790        assert_eq!(
791            std::error::Error::source(&captured).unwrap().to_string(),
792            "lookup failed"
793        );
794    }
795    #[test]
796    fn an_app_failure_can_never_report_shell_success() {
797        assert!(AppFailure::new(0, "").is_err());
798        for status in 1..=u8::MAX {
799            let failure = AppFailure::new(status, "domain error").expect("nonzero is accepted");
800            assert_ne!(failure.exit_status(), ExitStatus::SUCCESS);
801            assert_ne!(RunError::from(failure).exit_status(), ExitStatus::SUCCESS);
802        }
803    }
804    #[test]
805    #[should_panic(expected = "app run errors must be constructed from AppFailure")]
806    fn run_error_new_rejects_app_kind() {
807        let _ = RunError::new("inconsistent", RunErrorKind::App);
808    }
809    #[test]
810    fn test_command_context_default() {
811        let ctx = CommandContext::default();
812        assert!(ctx.command_path.is_empty());
813        assert!(ctx.extensions.is_empty());
814        assert!(ctx.app_state.is_empty());
815    }
816    #[test]
817    fn test_command_context_with_app_state() {
818        struct Database {
819            url: String,
820        }
821        struct Config {
822            debug: bool,
823        }
824        let mut app_state = Extensions::new();
825        app_state.insert(Database {
826            url: "postgres://localhost".into(),
827        });
828        app_state.insert(Config { debug: true });
829        let app_state = Rc::new(app_state);
830        let ctx = CommandContext {
831            command_path: vec!["list".into()],
832            app_state: app_state.clone(),
833            extensions: Extensions::new(),
834            stream: EntryStream::discarding(),
835        };
836        let db = ctx.app_state.get::<Database>().unwrap();
837        assert_eq!(db.url, "postgres://localhost");
838        let config = ctx.app_state.get::<Config>().unwrap();
839        assert!(config.debug);
840        assert_eq!(Rc::strong_count(&ctx.app_state), 2);
841    }
842    #[test]
843    fn test_command_context_app_state_get_required() {
844        struct Present;
845        let mut app_state = Extensions::new();
846        app_state.insert(Present);
847        let ctx = CommandContext {
848            command_path: vec![],
849            app_state: Rc::new(app_state),
850            extensions: Extensions::new(),
851            stream: EntryStream::discarding(),
852        };
853        assert!(ctx.app_state.get_required::<Present>().is_ok());
854        #[derive(Debug)]
855        struct Missing;
856        let err = ctx.app_state.get_required::<Missing>();
857        assert!(err.is_err());
858        assert!(err.unwrap_err().to_string().contains("Extension missing"));
859    }
860    #[test]
861    fn test_extensions_insert_and_get() {
862        struct MyState {
863            value: i32,
864        }
865        let mut ext = Extensions::new();
866        assert!(ext.is_empty());
867        ext.insert(MyState { value: 42 });
868        assert!(!ext.is_empty());
869        assert_eq!(ext.len(), 1);
870        let state = ext.get::<MyState>().unwrap();
871        assert_eq!(state.value, 42);
872    }
873    #[test]
874    fn test_extensions_get_mut() {
875        struct Counter {
876            count: i32,
877        }
878        let mut ext = Extensions::new();
879        ext.insert(Counter { count: 0 });
880        if let Some(counter) = ext.get_mut::<Counter>() {
881            counter.count += 1;
882        }
883        assert_eq!(ext.get::<Counter>().unwrap().count, 1);
884    }
885    #[test]
886    fn test_extensions_multiple_types() {
887        struct TypeA(i32);
888        struct TypeB(String);
889        let mut ext = Extensions::new();
890        ext.insert(TypeA(1));
891        ext.insert(TypeB("hello".into()));
892        assert_eq!(ext.len(), 2);
893        assert_eq!(ext.get::<TypeA>().unwrap().0, 1);
894        assert_eq!(ext.get::<TypeB>().unwrap().0, "hello");
895    }
896    #[test]
897    fn test_extensions_replace() {
898        struct Value(i32);
899        let mut ext = Extensions::new();
900        ext.insert(Value(1));
901        let old = ext.insert(Value(2));
902        assert_eq!(old.unwrap().0, 1);
903        assert_eq!(ext.get::<Value>().unwrap().0, 2);
904    }
905    #[test]
906    fn test_extensions_remove() {
907        struct Value(i32);
908        let mut ext = Extensions::new();
909        ext.insert(Value(42));
910        let removed = ext.remove::<Value>();
911        assert_eq!(removed.unwrap().0, 42);
912        assert!(ext.is_empty());
913        assert!(ext.get::<Value>().is_none());
914    }
915    #[test]
916    fn test_extensions_contains() {
917        struct Present;
918        struct Absent;
919        let mut ext = Extensions::new();
920        ext.insert(Present);
921        assert!(ext.contains::<Present>());
922        assert!(!ext.contains::<Absent>());
923    }
924    #[test]
925    fn test_extensions_clear() {
926        struct A;
927        struct B;
928        let mut ext = Extensions::new();
929        ext.insert(A);
930        ext.insert(B);
931        assert_eq!(ext.len(), 2);
932        ext.clear();
933        assert!(ext.is_empty());
934    }
935    #[test]
936    fn test_extensions_missing_type_returns_none() {
937        struct NotInserted;
938        let ext = Extensions::new();
939        assert!(ext.get::<NotInserted>().is_none());
940    }
941    #[test]
942    fn test_extensions_get_required() {
943        #[derive(Debug)]
944        struct Config {
945            value: i32,
946        }
947        let mut ext = Extensions::new();
948        ext.insert(Config { value: 100 });
949        let val = ext.get_required::<Config>();
950        assert!(val.is_ok());
951        assert_eq!(val.unwrap().value, 100);
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    #[test]
962    fn test_extensions_get_mut_required() {
963        #[derive(Debug)]
964        struct State {
965            count: i32,
966        }
967        let mut ext = Extensions::new();
968        ext.insert(State { count: 0 });
969        {
970            let val = ext.get_mut_required::<State>();
971            assert!(val.is_ok());
972            val.unwrap().count += 1;
973        }
974        assert_eq!(ext.get_required::<State>().unwrap().count, 1);
975        #[derive(Debug)]
976        struct Missing;
977        let err = ext.get_mut_required::<Missing>();
978        assert!(err.is_err());
979    }
980    #[test]
981    fn test_extensions_clone_behavior() {
982        struct Data(#[allow(dead_code)] i32);
983        let mut original = Extensions::new();
984        original.insert(Data(42));
985        let cloned = original.clone();
986        assert!(original.get::<Data>().is_some());
987        assert!(cloned.is_empty());
988        assert!(cloned.get::<Data>().is_none());
989    }
990    #[test]
991    fn test_output_render() {
992        let output: Output<String> = Output::Render("success".into());
993        assert!(output.is_render());
994        assert!(!output.is_silent());
995        assert!(!output.is_binary());
996    }
997    #[test]
998    fn test_output_silent() {
999        let output: Output<String> = Output::Silent;
1000        assert!(!output.is_render());
1001        assert!(output.is_silent());
1002        assert!(!output.is_binary());
1003    }
1004    #[test]
1005    fn a_declared_status_rides_beside_the_output_and_the_last_one_wins() {
1006        let plain: Output<String> = Output::Render("found nothing".into());
1007        assert_eq!(plain.exit_status(), ExitStatus::SUCCESS);
1008        assert_eq!(plain.split_exit_status().1, None);
1009
1010        let signalled = Output::Render(String::from("changes"))
1011            .with_exit_status(ExitStatus::from(3))
1012            .with_exit_status(ExitStatus::from(2));
1013        assert_eq!(signalled.exit_status(), ExitStatus::from(2));
1014        assert!(signalled.is_render());
1015        assert!(!signalled.is_silent());
1016
1017        let stamped = signalled.map_render(|text| format!("{text}!"));
1018        let (output, status) = stamped.split_exit_status();
1019        assert_eq!(status, Some(ExitStatus::from(2)));
1020        assert!(matches!(output, Output::Render(ref text) if text == "changes!"));
1021
1022        let silent: Output<()> = Output::Silent.with_exit_status(ExitStatus::from(4));
1023        assert!(silent.is_silent());
1024        assert_eq!(silent.split_exit_status().1, Some(ExitStatus::from(4)));
1025    }
1026    #[test]
1027    fn a_handled_run_reports_the_status_its_output_declared() {
1028        let handled = DispatchResult::Handled(
1029            RunOutput::command("plan").with_exit_status(ExitStatus::from(2)),
1030        );
1031        assert_eq!(handled.exit_status(), Some(ExitStatus::from(2)));
1032        assert_eq!(handled.success_kind(), Some(SuccessKind::Command));
1033        assert!(!handled.is_error());
1034        assert_eq!(
1035            DispatchResult::Handled(RunOutput::command("plan")).exit_status(),
1036            Some(ExitStatus::SUCCESS)
1037        );
1038    }
1039    #[test]
1040    fn test_output_binary() {
1041        let output: Output<String> = Output::Binary {
1042            data: vec![0x25, 0x50, 0x44, 0x46],
1043            filename: "report.pdf".into(),
1044        };
1045        assert!(!output.is_render());
1046        assert!(!output.is_silent());
1047        assert!(output.is_binary());
1048    }
1049    #[test]
1050    fn test_run_result_handled() {
1051        let result = DispatchResult::Handled("output".into());
1052        assert!(result.is_handled());
1053        assert!(!result.is_binary());
1054        assert!(!result.is_silent());
1055        assert_eq!(result.output(), Some("output"));
1056        assert!(result.matches().is_none());
1057    }
1058    #[test]
1059    fn test_run_result_silent() {
1060        let result = DispatchResult::Silent;
1061        assert!(!result.is_handled());
1062        assert!(!result.is_binary());
1063        assert!(result.is_silent());
1064    }
1065    #[test]
1066    fn test_run_result_binary() {
1067        let bytes = vec![0x25, 0x50, 0x44, 0x46];
1068        let result = DispatchResult::Binary(bytes.clone(), "report.pdf".into());
1069        assert!(!result.is_handled());
1070        assert!(result.is_binary());
1071        assert!(!result.is_silent());
1072        let (data, filename) = result.binary().unwrap();
1073        assert_eq!(data, &bytes);
1074        assert_eq!(filename, "report.pdf");
1075    }
1076    #[test]
1077    fn test_run_result_no_match() {
1078        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1079        let result = DispatchResult::NoMatch(matches);
1080        assert!(!result.is_handled());
1081        assert!(!result.is_binary());
1082        assert!(result.matches().is_some());
1083    }
1084    #[test]
1085    fn test_fn_handler() {
1086        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1087            Ok(Output::Render(json!({"status": "ok"})))
1088        });
1089        let ctx = CommandContext::default();
1090        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1091        let result = handler.handle(&matches, &ctx);
1092        assert!(result.is_ok());
1093    }
1094    #[test]
1095    fn test_fn_handler_mutation() {
1096        let mut counter = 0u32;
1097        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1098            counter += 1;
1099            Ok(Output::Render(counter))
1100        });
1101        let ctx = CommandContext::default();
1102        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1103        let _ = handler.handle(&matches, &ctx);
1104        let _ = handler.handle(&matches, &ctx);
1105        let result = handler.handle(&matches, &ctx);
1106        assert!(result.is_ok());
1107        if let Ok(Output::Render(count)) = result {
1108            assert_eq!(count, 3);
1109        }
1110    }
1111    #[test]
1112    fn test_into_handler_result_from_result_ok() {
1113        use super::IntoHandlerResult;
1114        let result: Result<String, anyhow::Error> = Ok("hello".to_string());
1115        let handler_result = result.into_handler_result();
1116        assert!(handler_result.is_ok());
1117        match handler_result.unwrap() {
1118            Output::Render(s) => assert_eq!(s, "hello"),
1119            _ => panic!("Expected Output::Render"),
1120        }
1121    }
1122    #[test]
1123    fn test_into_handler_result_from_result_err() {
1124        use super::IntoHandlerResult;
1125        let result: Result<String, anyhow::Error> = Err(anyhow::anyhow!("test error"));
1126        let handler_result = result.into_handler_result();
1127        assert!(handler_result.is_err());
1128        assert!(handler_result
1129            .unwrap_err()
1130            .to_string()
1131            .contains("test error"));
1132    }
1133    #[test]
1134    fn test_into_handler_result_passthrough_render() {
1135        use super::IntoHandlerResult;
1136        let handler_result: HandlerResult<String> = Ok(Output::Render("hello".to_string()));
1137        let result = handler_result.into_handler_result();
1138        assert!(result.is_ok());
1139        match result.unwrap() {
1140            Output::Render(s) => assert_eq!(s, "hello"),
1141            _ => panic!("Expected Output::Render"),
1142        }
1143    }
1144    #[test]
1145    fn test_into_handler_result_passthrough_silent() {
1146        use super::IntoHandlerResult;
1147        let handler_result: HandlerResult<String> = Ok(Output::Silent);
1148        let result = handler_result.into_handler_result();
1149        assert!(result.is_ok());
1150        assert!(matches!(result.unwrap(), Output::Silent));
1151    }
1152    #[test]
1153    fn test_into_handler_result_passthrough_binary() {
1154        use super::IntoHandlerResult;
1155        let handler_result: HandlerResult<String> = Ok(Output::Binary {
1156            data: vec![1, 2, 3],
1157            filename: "test.bin".to_string(),
1158        });
1159        let result = handler_result.into_handler_result();
1160        assert!(result.is_ok());
1161        match result.unwrap() {
1162            Output::Binary { data, filename } => {
1163                assert_eq!(data, vec![1, 2, 3]);
1164                assert_eq!(filename, "test.bin");
1165            }
1166            _ => panic!("Expected Output::Binary"),
1167        }
1168    }
1169    #[test]
1170    fn test_fn_handler_with_auto_wrap() {
1171        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1172            Ok::<_, anyhow::Error>("auto-wrapped".to_string())
1173        });
1174        let ctx = CommandContext::default();
1175        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1176        let result = handler.handle(&matches, &ctx);
1177        assert!(result.is_ok());
1178        match result.unwrap() {
1179            Output::Render(s) => assert_eq!(s, "auto-wrapped"),
1180            _ => panic!("Expected Output::Render"),
1181        }
1182    }
1183    #[test]
1184    fn test_fn_handler_with_explicit_output() {
1185        let mut handler =
1186            FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| Ok(Output::<()>::Silent));
1187        let ctx = CommandContext::default();
1188        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1189        let result = handler.handle(&matches, &ctx);
1190        assert!(result.is_ok());
1191        assert!(matches!(result.unwrap(), Output::Silent));
1192    }
1193    #[test]
1194    fn test_fn_handler_with_custom_error_type() {
1195        #[derive(Debug)]
1196        struct CustomError(String);
1197        impl std::fmt::Display for CustomError {
1198            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1199                write!(f, "CustomError: {}", self.0)
1200            }
1201        }
1202        impl std::error::Error for CustomError {}
1203        let mut handler = FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
1204            Err::<String, CustomError>(CustomError("oops".to_string()))
1205        });
1206        let ctx = CommandContext::default();
1207        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1208        let result = handler.handle(&matches, &ctx);
1209        assert!(result.is_err());
1210        assert!(result
1211            .unwrap_err()
1212            .to_string()
1213            .contains("CustomError: oops"));
1214    }
1215    #[test]
1216    fn test_simple_fn_handler_basic() {
1217        use super::SimpleFnHandler;
1218        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1219            Ok::<_, anyhow::Error>("no context needed".to_string())
1220        });
1221        let ctx = CommandContext::default();
1222        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1223        let result = handler.handle(&matches, &ctx);
1224        assert!(result.is_ok());
1225        match result.unwrap() {
1226            Output::Render(s) => assert_eq!(s, "no context needed"),
1227            _ => panic!("Expected Output::Render"),
1228        }
1229    }
1230    #[test]
1231    fn test_simple_fn_handler_with_args() {
1232        use super::SimpleFnHandler;
1233        let mut handler = SimpleFnHandler::new(|m: &ArgMatches| {
1234            let verbose = m.get_flag("verbose");
1235            Ok::<_, anyhow::Error>(verbose)
1236        });
1237        let ctx = CommandContext::default();
1238        let matches = clap::Command::new("test")
1239            .arg(
1240                clap::Arg::new("verbose")
1241                    .short('v')
1242                    .action(clap::ArgAction::SetTrue),
1243            )
1244            .get_matches_from(vec!["test", "-v"]);
1245        let result = handler.handle(&matches, &ctx);
1246        assert!(result.is_ok());
1247        match result.unwrap() {
1248            Output::Render(v) => assert!(v),
1249            _ => panic!("Expected Output::Render"),
1250        }
1251    }
1252    #[test]
1253    fn test_simple_fn_handler_explicit_output() {
1254        use super::SimpleFnHandler;
1255        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| Ok(Output::<()>::Silent));
1256        let ctx = CommandContext::default();
1257        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1258        let result = handler.handle(&matches, &ctx);
1259        assert!(result.is_ok());
1260        assert!(matches!(result.unwrap(), Output::Silent));
1261    }
1262    #[test]
1263    fn test_simple_fn_handler_error() {
1264        use super::SimpleFnHandler;
1265        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1266            Err::<String, _>(anyhow::anyhow!("simple error"))
1267        });
1268        let ctx = CommandContext::default();
1269        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1270        let result = handler.handle(&matches, &ctx);
1271        assert!(result.is_err());
1272        assert!(result.unwrap_err().to_string().contains("simple error"));
1273    }
1274    #[test]
1275    fn test_simple_fn_handler_mutation() {
1276        use super::SimpleFnHandler;
1277        let mut counter = 0u32;
1278        let mut handler = SimpleFnHandler::new(|_m: &ArgMatches| {
1279            counter += 1;
1280            Ok::<_, anyhow::Error>(counter)
1281        });
1282        let ctx = CommandContext::default();
1283        let matches = clap::Command::new("test").get_matches_from(vec!["test"]);
1284        let _ = handler.handle(&matches, &ctx);
1285        let _ = handler.handle(&matches, &ctx);
1286        let result = handler.handle(&matches, &ctx);
1287        assert!(result.is_ok());
1288        match result.unwrap() {
1289            Output::Render(n) => assert_eq!(n, 3),
1290            _ => panic!("Expected Output::Render"),
1291        }
1292    }
1293
1294    #[test]
1295    fn a_carried_diagnostic_wins_and_takes_the_framework_kind() {
1296        let carried = Diagnostic::error("line 2 does not parse")
1297            .detail("expected `resource <name> <state>`")
1298            .range("main.tfl", 2, 1);
1299        let error = RunError::new("Error: line 2 does not parse", RunErrorKind::Handler)
1300            .with_diagnostic(carried.clone());
1301        let diagnostic = error.diagnostic();
1302        assert_eq!(diagnostic.kind, DiagnosticKind::Handler);
1303        assert_eq!(diagnostic.severity, Severity::Error);
1304        assert_eq!(diagnostic.summary, carried.summary);
1305        assert_eq!(diagnostic.detail, carried.detail);
1306        assert_eq!(diagnostic.range, carried.range);
1307        let mut hook_carried = Diagnostic::warning("soft");
1308        hook_carried.kind = DiagnosticKind::ClapUsage;
1309        let hook = RunError::new("Error: soft", RunErrorKind::Hook(HookPhase::PostDispatch))
1310            .with_diagnostic(hook_carried);
1311        let hook = hook.diagnostic();
1312        assert_eq!(hook.kind, DiagnosticKind::HookPostDispatch);
1313        assert_eq!(hook.severity, Severity::Error);
1314    }
1315    #[test]
1316    fn a_prose_error_splits_into_summary_and_detail_without_its_framing() {
1317        let clap = RunError::new(
1318            "error: unexpected argument '--bogus' found\n\nUsage: app [OPTIONS]\n\nFor more information, try '--help'.\n",
1319            RunErrorKind::ClapUsage,
1320        )
1321        .diagnostic();
1322        assert_eq!(clap.kind, DiagnosticKind::ClapUsage);
1323        assert_eq!(clap.summary, "unexpected argument '--bogus' found");
1324        assert_eq!(
1325            clap.detail,
1326            "Usage: app [OPTIONS]\n\nFor more information, try '--help'."
1327        );
1328        assert_eq!(clap.range, None);
1329        let framed =
1330            RunError::new("Error: could not read config", RunErrorKind::Render).diagnostic();
1331        assert_eq!(framed.summary, "could not read config");
1332        assert_eq!(framed.detail, "");
1333        let bare = RunError::new("plain", RunErrorKind::FinalWrite(OutputKind::Text)).diagnostic();
1334        assert_eq!(bare.summary, "plain");
1335        assert_eq!(bare.kind, DiagnosticKind::FinalWrite);
1336    }
1337    #[test]
1338    fn owner_declared_failures_keep_their_bytes_as_detail() {
1339        let app = RunError::from(
1340            AppFailure::new(3, "ghlike: not found: demo/gamma\nsee --help\n").unwrap(),
1341        )
1342        .diagnostic();
1343        assert_eq!(app.kind, DiagnosticKind::App);
1344        assert_eq!(app.summary, "ghlike: not found: demo/gamma");
1345        assert_eq!(app.detail, "ghlike: not found: demo/gamma\nsee --help\n");
1346        let external = RunError::from(ExternalFailure::new(128, "").unwrap()).diagnostic();
1347        assert_eq!(external.kind, DiagnosticKind::External);
1348        assert_eq!(external.summary, "");
1349        assert_eq!(external.detail, "");
1350    }
1351}