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