use std::io;
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CanonicalError {
pub kind: ErrorKind,
pub message: String,
#[serde(default)]
pub provider_detail: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retry_after_seconds: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
Usage,
ParseInput,
Config,
Auth,
Provider {
status: u16,
},
Transport,
Interrupted,
Other(String),
}
#[derive(Serialize)]
struct ProviderBody {
status: u16,
}
impl Serialize for ErrorKind {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
match self {
ErrorKind::Usage => s.serialize_str("usage"),
ErrorKind::ParseInput => s.serialize_str("parse_input"),
ErrorKind::Config => s.serialize_str("config"),
ErrorKind::Auth => s.serialize_str("auth"),
ErrorKind::Transport => s.serialize_str("transport"),
ErrorKind::Interrupted => s.serialize_str("interrupted"),
ErrorKind::Provider { status } => {
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("provider", &ProviderBody { status: *status })?;
m.end()
}
ErrorKind::Other(tag) => s.serialize_str(tag),
}
}
}
impl<'de> Deserialize<'de> for ErrorKind {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let v = Value::deserialize(d)?;
let tag = match &v {
Value::String(s) => s.as_str(),
other => other
.as_object()
.and_then(|m| m.keys().next())
.map_or("", String::as_str),
};
Ok(match tag {
"usage" => ErrorKind::Usage,
"parse_input" => ErrorKind::ParseInput,
"config" => ErrorKind::Config,
"auth" => ErrorKind::Auth,
"transport" => ErrorKind::Transport,
"interrupted" => ErrorKind::Interrupted,
"provider" => ErrorKind::Provider {
status: v["provider"]["status"].as_u64().unwrap_or_default() as u16,
},
_ => ErrorKind::Other(tag.to_owned()),
})
}
}
impl ErrorKind {
pub fn from_http_status(status: u16) -> ErrorKind {
match status {
401 | 403 => ErrorKind::Auth,
_ => ErrorKind::Provider { status },
}
}
pub fn http_status(&self) -> u16 {
match self {
ErrorKind::Provider { status } => *status,
ErrorKind::Auth => 401,
ErrorKind::Usage | ErrorKind::ParseInput => 400,
ErrorKind::Transport => 502,
ErrorKind::Config | ErrorKind::Interrupted | ErrorKind::Other(_) => 500,
}
}
}
impl CanonicalError {
pub fn retryable(&self) -> bool {
matches!(self.kind, ErrorKind::Transport)
|| matches!(self.kind, ErrorKind::Provider { status } if status == 429 || status >= 500)
}
pub fn exit_code(&self) -> u8 {
ExitClass::from_kind(self.kind.clone()).code()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExitClass {
Ok,
Usage,
NoInput,
Unavailable,
Software,
NoPerm,
Config,
Sig(i32),
}
impl ExitClass {
pub fn code(self) -> u8 {
match self {
ExitClass::Ok => 0,
ExitClass::Usage => 64,
ExitClass::NoInput => 66,
ExitClass::Unavailable => 69,
ExitClass::Software => 70,
ExitClass::NoPerm => 77,
ExitClass::Config => 78,
ExitClass::Sig(n) => n as u8,
}
}
pub fn from_kind(kind: ErrorKind) -> ExitClass {
match kind {
ErrorKind::Usage | ErrorKind::ParseInput => ExitClass::Usage,
ErrorKind::Config => ExitClass::Config,
ErrorKind::Auth => ExitClass::NoPerm,
ErrorKind::Provider { status } if status >= 500 => ExitClass::Software,
ErrorKind::Provider { .. } => ExitClass::Unavailable,
ErrorKind::Transport => ExitClass::Unavailable,
ErrorKind::Interrupted => ExitClass::Sig(130),
ErrorKind::Other(_) => ExitClass::Software,
}
}
pub fn from_io(e: &io::Error) -> ExitClass {
match e.kind() {
io::ErrorKind::BrokenPipe => ExitClass::Sig(141),
_ => ExitClass::Unavailable,
}
}
}