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