Skip to main content

xbp_cli/cli/
error.rs

1use clap::error::ErrorKind as ClapErrorKind;
2use colored::Colorize;
3use thiserror::Error;
4
5/// Unified CLI error type to keep propagation consistent across handlers.
6#[derive(Debug, Error)]
7pub enum CliError {
8    #[error("{0}")]
9    Message(String),
10    #[error(transparent)]
11    Anyhow(#[from] anyhow::Error),
12}
13
14pub type CliResult<T> = Result<T, CliError>;
15
16#[derive(Debug, Clone, Copy)]
17pub enum ErrorKindTag {
18    Parse,
19    Validation,
20    Dependency,
21    Permission,
22    Operation,
23    Config,
24    Runtime,
25}
26
27impl ErrorKindTag {
28    fn label(self) -> &'static str {
29        match self {
30            ErrorKindTag::Parse => "PARSE",
31            ErrorKindTag::Validation => "VALIDATION",
32            ErrorKindTag::Dependency => "DEPENDENCY",
33            ErrorKindTag::Permission => "PERMISSION",
34            ErrorKindTag::Operation => "OPERATION",
35            ErrorKindTag::Config => "CONFIG",
36            ErrorKindTag::Runtime => "RUNTIME",
37        }
38    }
39}
40
41/// Centralized CLI error factory used across handlers for consistent UX.
42pub struct ErrorFactory;
43
44impl ErrorFactory {
45    pub fn parse(message: impl Into<String>, hint: Option<&str>) -> CliError {
46        Self::build(ErrorKindTag::Parse, "cli", message, hint, None)
47    }
48
49    pub fn validation(component: &str, message: impl Into<String>, hint: Option<&str>) -> CliError {
50        Self::build(ErrorKindTag::Validation, component, message, hint, None)
51    }
52
53    pub fn dependency(component: &str, message: impl Into<String>, hint: Option<&str>) -> CliError {
54        Self::build(ErrorKindTag::Dependency, component, message, hint, None)
55    }
56
57    pub fn permission(component: &str, message: impl Into<String>, hint: Option<&str>) -> CliError {
58        Self::build(ErrorKindTag::Permission, component, message, hint, None)
59    }
60
61    pub fn config(component: &str, message: impl Into<String>, hint: Option<&str>) -> CliError {
62        Self::build(ErrorKindTag::Config, component, message, hint, None)
63    }
64
65    pub fn operation(
66        component: &str,
67        action: &str,
68        source: impl Into<String>,
69        hint: Option<&str>,
70    ) -> CliError {
71        let message = format!("{} failed", action);
72        Self::build(
73            ErrorKindTag::Operation,
74            component,
75            message,
76            hint,
77            Some(source.into()),
78        )
79    }
80
81    pub fn runtime(component: &str, message: impl Into<String>, hint: Option<&str>) -> CliError {
82        Self::build(ErrorKindTag::Runtime, component, message, hint, None)
83    }
84
85    pub fn clap_parse(err: clap::Error) -> CliError {
86        let hint = Self::clap_hint(err.kind());
87        Self::parse(err.to_string(), hint)
88    }
89
90    fn clap_hint(kind: ClapErrorKind) -> Option<&'static str> {
91        match kind {
92            ClapErrorKind::InvalidSubcommand => {
93                Some("Run `xbp --help` to list available commands.")
94            }
95            ClapErrorKind::UnknownArgument | ClapErrorKind::InvalidValue => {
96                Some("Use `-h` with your subcommand to see supported options.")
97            }
98            ClapErrorKind::MissingRequiredArgument => {
99                Some("Check the usage block above and provide required arguments.")
100            }
101            _ => None,
102        }
103    }
104
105    fn build(
106        kind: ErrorKindTag,
107        component: &str,
108        message: impl Into<String>,
109        hint: Option<&str>,
110        details: Option<String>,
111    ) -> CliError {
112        let header = format!("[{}] {}", kind.label(), component)
113            .bright_red()
114            .bold();
115        let mut lines = vec![header.to_string(), format!("  {}", message.into())];
116        if let Some(details) = details {
117            lines.push(format!("  {} {}", "Details:".bright_black(), details));
118        }
119        if let Some(hint) = hint {
120            lines.push(format!("  {} {}", "Hint:".bright_yellow().bold(), hint));
121        }
122        CliError::Message(lines.join("\n"))
123    }
124}
125
126impl From<String> for CliError {
127    fn from(value: String) -> Self {
128        CliError::Message(value)
129    }
130}
131
132impl From<&str> for CliError {
133    fn from(value: &str) -> Self {
134        CliError::Message(value.to_string())
135    }
136}