use std::fmt;
use std::time::Duration;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
InvalidConfiguration(String),
Validation(Box<Failure>),
Auth(Box<Failure>),
Quota(Box<Failure>),
Conflict(Box<Failure>),
Network(Box<Failure>),
Timeout(Box<Failure>),
Server(Box<Failure>),
Http(Box<Failure>),
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct Failure {
pub status: Option<u16>,
pub code: String,
pub message: String,
pub fields: Vec<FieldError>,
pub local: bool,
pub request_id: Option<String>,
pub idempotency_key: Option<String>,
pub attempts: u32,
pub retry_after: Option<Duration>,
pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
#[non_exhaustive]
pub struct FieldError {
pub field: String,
pub code: String,
#[serde(default)]
pub message: String,
}
impl FieldError {
pub(crate) fn new(field: impl Into<String>, code: &str, message: impl Into<String>) -> Self {
FieldError {
field: field.into(),
code: code.to_owned(),
message: message.into(),
}
}
}
impl Error {
pub fn failure(&self) -> Option<&Failure> {
match self {
Error::InvalidConfiguration(_) => None,
Error::Validation(f)
| Error::Auth(f)
| Error::Quota(f)
| Error::Conflict(f)
| Error::Network(f)
| Error::Timeout(f)
| Error::Server(f)
| Error::Http(f) => Some(&**f),
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
Error::Network(_) | Error::Timeout(_) | Error::Server(_) | Error::Quota(_)
)
}
pub fn status(&self) -> Option<u16> {
self.failure().and_then(|f| f.status)
}
pub fn code(&self) -> Option<&str> {
self.failure()
.map(|f| f.code.as_str())
.filter(|c| !c.is_empty())
}
pub fn fields(&self) -> &[FieldError] {
self.failure().map_or(&[], |f| f.fields.as_slice())
}
pub fn idempotency_key(&self) -> Option<&str> {
self.failure().and_then(|f| f.idempotency_key.as_deref())
}
pub fn retry_after(&self) -> Option<Duration> {
self.failure().and_then(|f| f.retry_after)
}
pub(crate) fn local_validation(fields: Vec<FieldError>) -> Error {
Error::Validation(Box::new(Failure {
code: "validation_failed".into(),
message: "invalid message".into(),
fields,
local: true,
..Failure::default()
}))
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let failure = match self {
Error::InvalidConfiguration(msg) => return write!(f, "honk: {msg}"),
_ => self.failure().expect("every other variant has a failure"),
};
f.write_str("honk: ")?;
if let Some(status) = failure.status {
write!(f, "{status} ")?;
if !failure.code.is_empty() {
write!(f, "{}: ", failure.code)?;
}
}
f.write_str(&failure.message)?;
if !failure.fields.is_empty() {
let parts: Vec<String> = failure
.fields
.iter()
.map(|e| {
format!(
"{} {}",
e.field,
if e.message.is_empty() {
&e.code
} else {
&e.message
}
)
})
.collect();
write!(f, " ({})", parts.join("; "))?;
}
Ok(())
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.failure()
.and_then(|f| f.source.as_deref())
.map(|e| e as &(dyn std::error::Error + 'static))
}
}