use std::borrow::Cow;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, CliCoreError>;
pub trait ExitCoder {
fn exit_code(&self) -> i32;
}
pub trait DetailedError: std::error::Error {
fn error_code(&self) -> Cow<'static, str>;
fn error_system(&self) -> Option<Cow<'static, str>>;
fn error_request_id(&self) -> Option<Cow<'static, str>>;
}
#[derive(Debug, Error)]
pub enum CliCoreError {
#[error("auth: no provider registered with name {0:?}")]
MissingAuthProvider(String),
#[error("auth: provider {provider:?}: {source}")]
AuthProvider {
provider: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("invalid output format {0:?}: must be one of toon, json, human")]
InvalidOutputFormat(String),
#[error("{0}")]
Message(String),
#[error("{message}")]
SystemMessage {
message: String,
system: String,
code: String,
request_id: String,
},
#[error("{source}")]
System {
system: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("{source}")]
Detailed {
code: String,
system: String,
request_id: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("{source}")]
ExitCode {
code: i32,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Transport(#[from] crate::transport::Error),
}
impl CliCoreError {
#[must_use]
pub fn message(message: impl Into<String>) -> Self {
Self::Message(message.into())
}
#[must_use]
pub fn message_for_system(system: impl Into<String>, message: impl Into<String>) -> Self {
Self::SystemMessage {
message: message.into(),
system: system.into(),
code: "ERROR".to_owned(),
request_id: String::new(),
}
}
#[must_use]
pub fn with_system(
system: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::System {
system: system.into(),
source: Box::new(source),
}
}
#[must_use]
pub fn with_exit_code(
code: i32,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::ExitCode {
code,
source: Box::new(source),
}
}
#[must_use]
pub fn with_detailed_error(source: impl DetailedError + Send + Sync + 'static) -> Self {
let code = source.error_code().into_owned();
let system = source
.error_system()
.map_or_else(String::new, Cow::into_owned);
let request_id = source
.error_request_id()
.map_or_else(String::new, Cow::into_owned);
Self::Detailed {
code,
system,
request_id,
source: Box::new(source),
}
}
#[must_use]
pub fn system(&self) -> Option<&str> {
match self {
Self::SystemMessage { system, .. }
| Self::System { system, .. }
| Self::Detailed { system, .. }
if !system.is_empty() =>
{
Some(system)
}
Self::MissingAuthProvider(_)
| Self::AuthProvider { .. }
| Self::InvalidOutputFormat(_)
| Self::Message(_)
| Self::SystemMessage { .. }
| Self::System { .. }
| Self::Detailed { .. }
| Self::ExitCode { .. }
| Self::Io(_)
| Self::Json(_)
| Self::Transport(_) => None,
}
}
}
impl ExitCoder for CliCoreError {
fn exit_code(&self) -> i32 {
exit_code_for_error(self)
}
}
#[must_use]
pub fn exit_code_for_exit_coder(err: &dyn ExitCoder) -> i32 {
err.exit_code()
}
#[must_use]
pub fn exit_code_for_error(err: &(dyn std::error::Error + 'static)) -> i32 {
let mut current = Some(err);
while let Some(error) = current {
if let Some(CliCoreError::ExitCode { code, .. }) = error.downcast_ref::<CliCoreError>() {
return *code;
}
current = error.source();
}
let mut current = Some(err);
while let Some(error) = current {
if let Some(cli_err) = error.downcast_ref::<CliCoreError>() {
match cli_err {
CliCoreError::MissingAuthProvider(_) | CliCoreError::AuthProvider { .. } => {
return 2;
}
CliCoreError::InvalidOutputFormat(_) => return 3,
CliCoreError::System { .. }
| CliCoreError::Detailed { .. }
| CliCoreError::ExitCode { .. }
| CliCoreError::Message(_)
| CliCoreError::SystemMessage { .. }
| CliCoreError::Io(_)
| CliCoreError::Json(_)
| CliCoreError::Transport(_) => {}
}
}
current = error.source();
}
let msg = err.to_string().to_lowercase();
if msg.contains("auth") {
2
} else if msg.contains("validation") || msg.contains("invalid") {
3
} else if msg.contains("not found") {
4
} else if msg.contains("permission") || msg.contains("forbidden") {
5
} else if msg.contains("denied") {
6
} else {
1
}
}