use std::collections::HashMap;
use std::error::Error as StdError;
use std::fmt;
use std::sync::Arc;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorKind {
NotFound,
Timeout,
InvalidInput,
PermissionDenied,
Io,
Api,
Parse,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorStatus {
Permanent,
Temporary,
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Error {
pub kind: ErrorKind,
pub status: ErrorStatus,
pub message: String,
pub context: Vec<(&'static str, String)>,
pub source: Option<Arc<dyn StdError + Send + Sync>>,
}
impl Error {
pub fn with(mut self, key: &'static str, value: impl Into<String>) -> Self {
self.context.push((key, value.into()));
self
}
pub fn with_source(self, source: impl StdError + Send + Sync + 'static) -> Self {
Self {
source: Some(Arc::new(source)),
..self
}
}
#[must_use]
pub fn display_for_user(&self) -> String {
let mut base = self.message.clone();
if !self.context.is_empty() {
let mut by_key: HashMap<&'static str, String> = HashMap::new();
for (k, v) in &self.context {
by_key.insert(*k, v.clone());
}
let mut keys: Vec<&'static str> = by_key.keys().copied().collect();
keys.sort();
let ctx: String = keys
.iter()
.map(|k| format!("{k}: {}", by_key.get(*k).map(String::as_str).unwrap_or("")))
.collect::<Vec<_>>()
.join(", ");
base = format!("{base} ({ctx})");
}
let mut chain = String::new();
let mut src: Option<&(dyn StdError + 'static)> = self.source();
while let Some(s) = src {
let msg = s.to_string();
if !msg.is_empty() {
if !chain.is_empty() {
chain.push_str(" — ");
}
chain.push_str(&msg);
}
src = s.source();
}
if chain.is_empty() {
base
} else {
format!("{base} — {chain}")
}
}
#[must_use]
pub fn is_retryable(&self) -> bool {
self.status == ErrorStatus::Temporary
}
fn new(
kind: ErrorKind,
status: ErrorStatus,
message: impl Into<String>,
source: Option<Arc<dyn StdError + Send + Sync>>,
) -> Self {
Self {
kind,
status,
message: message.into(),
context: Vec::new(),
source,
}
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(ErrorKind::NotFound, ErrorStatus::Permanent, message, None)
}
pub fn timeout(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Timeout, ErrorStatus::Temporary, message, None)
}
pub fn invalid_input(message: impl Into<String>) -> Self {
Self::new(
ErrorKind::InvalidInput,
ErrorStatus::Permanent,
message,
None,
)
}
pub fn permission_denied(message: impl Into<String>) -> Self {
Self::new(
ErrorKind::PermissionDenied,
ErrorStatus::Permanent,
message,
None,
)
}
pub fn io(message: impl Into<String>, source: impl StdError + Send + Sync + 'static) -> Self {
Self::new(
ErrorKind::Io,
ErrorStatus::Permanent,
message,
Some(Arc::new(source)),
)
}
pub fn api(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Api, ErrorStatus::Temporary, message, None)
}
pub fn api_permanent(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Api, ErrorStatus::Permanent, message, None)
}
pub fn parse(
message: impl Into<String>,
source: impl StdError + Send + Sync + 'static,
) -> Self {
Self::new(
ErrorKind::Parse,
ErrorStatus::Permanent,
message,
Some(Arc::new(source)),
)
}
#[must_use]
pub fn from_transfer_sdk_status(message: impl Into<String>, status: Option<u16>) -> Self {
match status {
Some(429) | Some(500..=599) => Self::api(message),
_ => Self::api_permanent(message),
}
}
#[must_use]
pub fn from_s3_sdk_status(message: impl Into<String>, status: Option<u16>) -> Self {
match status {
Some(404) => Self::not_found(message),
Some(429) | Some(500..=599) => Self::api(message),
_ => Self::api_permanent(message),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display_for_user())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source
.as_ref()
.map(|s| s.as_ref() as &(dyn StdError + 'static))
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::parse("parse error", e)
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::io("IO error", e)
}
}