use std::{
error::Error,
fmt::{Debug, Display},
};
use strum::{EnumIter, IntoEnumIterator};
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)]
pub enum FaithErrorKind {
Aborted,
AddressParse,
BodyStream,
Closed,
Config,
ContentLengthOverrun,
FileExists,
FileWrite,
IntegrityMismatch,
InvalidCompression,
InvalidHeader,
InvalidIntegrity,
InvalidMethod,
InvalidPath,
InvalidUrl,
JsonParse,
Network,
PemParse,
Redirect,
ResponseAlreadyDisturbed,
ResponseBodyNull,
Timeout,
}
impl FaithErrorKind {
pub fn code(self) -> String {
format!("{self:?}")
}
pub(crate) fn default_message(self) -> &'static str {
match self {
Self::Aborted => "the request was aborted",
Self::AddressParse => "invalid IP address and/or port",
Self::BodyStream => "internal response body stream copy error",
Self::Closed => "the agent has been closed",
Self::Config => "invalid agent configuration",
Self::ContentLengthOverrun => "response body exceeded the advertised Content-Length",
Self::FileExists => "the destination file already exists",
Self::FileWrite => "could not write the destination file",
Self::IntegrityMismatch => "resource integrity check failed",
Self::InvalidCompression => "invalid request body compression",
Self::InvalidHeader => "invalid header name or value",
Self::InvalidIntegrity => "invalid integrity value",
Self::InvalidMethod => "invalid HTTP method",
Self::InvalidPath => "destination does not name a local path",
Self::InvalidUrl => "invalid URL",
Self::JsonParse => "invalid json in response body",
Self::Network => "network error",
Self::PemParse => "invalid client certificate or key",
Self::Redirect => "got a redirect",
Self::ResponseAlreadyDisturbed => "response body already disturbed",
Self::ResponseBodyNull => "response cannot carry a body to write",
Self::Timeout => "timed out",
}
}
}
pub fn error_codes() -> Vec<String> {
FaithErrorKind::iter().map(FaithErrorKind::code).collect()
}
#[derive(Debug, Clone)]
pub struct FaithError {
pub kind: FaithErrorKind,
pub message: Option<String>,
}
impl FaithError {
pub fn new(kind: FaithErrorKind, message: Option<impl Into<String>>) -> Self {
Self {
kind,
message: message.map(|m| m.into()),
}
}
}
impl From<FaithErrorKind> for FaithError {
fn from(kind: FaithErrorKind) -> Self {
Self {
kind,
message: None,
}
}
}
fn faith_kind_in_chain(err: &(dyn Error + 'static)) -> Option<FaithErrorKind> {
let mut source = err.source();
while let Some(e) = source {
if let Some(faith) = e.downcast_ref::<FaithError>() {
return Some(faith.kind);
}
source = e.source();
}
None
}
impl From<std::convert::Infallible> for FaithError {
fn from(never: std::convert::Infallible) -> Self {
match never {}
}
}
impl From<reqwest::Error> for FaithError {
fn from(err: reqwest::Error) -> Self {
let mut msg = format!("{err:?}");
let mut source = err.source();
while let Some(e) = source {
msg.push_str(&format!(" -> {e:?}"));
source = e.source();
}
if err.is_timeout() {
return FaithError::new(FaithErrorKind::Timeout, Some(msg));
}
let kind = err
.is_redirect()
.then(|| faith_kind_in_chain(&err))
.flatten()
.unwrap_or(FaithErrorKind::Network);
FaithError::new(kind, Some(msg))
}
}
impl From<reqwest_middleware::Error> for FaithError {
fn from(err: reqwest_middleware::Error) -> Self {
match err {
reqwest_middleware::Error::Middleware(err) => {
FaithError::new(FaithErrorKind::Network, Some(err.to_string()))
}
reqwest_middleware::Error::Reqwest(err) => err.into(),
}
}
}
impl Error for FaithError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
fn description(&self) -> &str {
"description() is deprecated; use Display"
}
fn cause(&self) -> Option<&dyn Error> {
self.source()
}
}
impl Display for FaithError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:?}: {}",
self.kind,
self.message
.as_deref()
.unwrap_or_else(|| self.kind.default_message())
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_code_is_distinct_and_named() {
let codes = error_codes();
let unique: std::collections::BTreeSet<_> = codes.iter().collect();
assert_eq!(unique.len(), codes.len(), "two kinds report the same code");
assert!(codes.iter().all(|code| !code.is_empty()));
}
#[test]
fn a_message_is_prefixed_with_the_code_it_reports() {
for kind in FaithErrorKind::iter() {
let code = kind.code();
let rendered = FaithError::from(kind).to_string();
assert!(
rendered.starts_with(&format!("{code}: ")),
"{rendered} does not lead with {code}"
);
}
}
#[test]
fn a_kind_without_a_message_falls_back_to_its_own() {
let err = FaithError::from(FaithErrorKind::Closed);
assert_eq!(err.to_string(), "Closed: the agent has been closed");
let err = FaithError::new(FaithErrorKind::Closed, Some("gone"));
assert_eq!(err.to_string(), "Closed: gone");
}
}