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 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 pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
465 self.diagnostic = Some(Box::new(diagnostic));
466 self
467 }
468 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 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}