1use clap::error::ErrorKind as ClapErrorKind;
2use colored::Colorize;
3use thiserror::Error;
4
5#[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
41pub 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}