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