specman_cli/
error.rs

1use std::fmt;
2use std::process::ExitCode;
3
4use clap::error::ErrorKind as ClapErrorKind;
5use specman::SpecmanError;
6use specman::error::LifecycleError;
7
8const EX_OK: u8 = 0;
9const EX_USAGE: u8 = 64;
10const EX_DATAERR: u8 = 65;
11const EX_SOFTWARE: u8 = 70;
12const EX_OSERR: u8 = 71;
13const EX_CONFIG: u8 = 78;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ExitStatus {
17    Ok,
18    Usage,
19    Data,
20    Io,
21    Config,
22    Software,
23}
24
25impl ExitStatus {
26    pub fn code(self) -> u8 {
27        match self {
28            ExitStatus::Ok => EX_OK,
29            ExitStatus::Usage => EX_USAGE,
30            ExitStatus::Data => EX_DATAERR,
31            ExitStatus::Io => EX_OSERR,
32            ExitStatus::Config => EX_CONFIG,
33            ExitStatus::Software => EX_SOFTWARE,
34        }
35    }
36}
37
38#[derive(Debug)]
39pub struct CliError {
40    message: String,
41    status: ExitStatus,
42}
43
44impl CliError {
45    pub fn new(message: impl Into<String>, status: ExitStatus) -> Self {
46        Self {
47            message: message.into(),
48            status,
49        }
50    }
51
52    pub fn exit_code(&self) -> ExitCode {
53        ExitCode::from(self.status.code())
54    }
55
56    pub fn print(&self) {
57        if !self.message.is_empty() {
58            eprintln!("{}", self.message);
59        }
60    }
61}
62
63impl From<SpecmanError> for CliError {
64    fn from(err: SpecmanError) -> Self {
65        let status = match &err {
66            SpecmanError::Template(_)
67            | SpecmanError::Dependency(_)
68            | SpecmanError::MissingTarget(_) => ExitStatus::Data,
69            SpecmanError::UnknownWorkType(_) => ExitStatus::Usage,
70            SpecmanError::Lifecycle(err) => match err {
71                LifecycleError::DeletionBlocked { .. } => ExitStatus::Data,
72                LifecycleError::PlanTargetMismatch { .. } => ExitStatus::Software,
73                LifecycleError::Context { source, .. } => match source.as_ref() {
74                    LifecycleError::DeletionBlocked { .. } => ExitStatus::Data,
75                    LifecycleError::PlanTargetMismatch { .. } => ExitStatus::Software,
76                    LifecycleError::Context { .. } => ExitStatus::Software,
77                },
78            },
79            SpecmanError::Workspace(_) => ExitStatus::Usage,
80            SpecmanError::Serialization(_) => ExitStatus::Software,
81            SpecmanError::Io(_) => ExitStatus::Io,
82        };
83        CliError::new(err.to_string(), status)
84    }
85}
86
87#[cfg(test)]
88#[test]
89fn unknown_work_type_maps_to_usage() {
90    let err = CliError::from(SpecmanError::UnknownWorkType("nope".to_string()));
91    assert_eq!(err.status, ExitStatus::Usage);
92    assert_eq!(err.status.code(), EX_USAGE);
93}
94
95impl From<clap::Error> for CliError {
96    fn from(err: clap::Error) -> Self {
97        let status = match err.kind() {
98            ClapErrorKind::DisplayHelp | ClapErrorKind::DisplayVersion => ExitStatus::Ok,
99            _ => ExitStatus::Usage,
100        };
101        if status == ExitStatus::Ok {
102            let _ = err.print();
103            CliError::new(String::new(), status)
104        } else {
105            CliError::new(err.to_string(), status)
106        }
107    }
108}
109
110impl From<std::io::Error> for CliError {
111    fn from(err: std::io::Error) -> Self {
112        CliError::new(err.to_string(), ExitStatus::Io)
113    }
114}
115
116impl fmt::Display for CliError {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(f, "{}", self.message)
119    }
120}
121
122impl std::error::Error for CliError {}