use super::models::Problem;
use super::models::ProblemId;
const LOWEST_SUCCESS: u16 = 200;
const LOWEST_REDIRECTION: u16 = 300;
const MAX_PREVIEW_BYTES: usize = 256;
#[derive(Debug, Clone)]
pub struct ProblemError {
pub status: u16,
pub kind: Option<ProblemId>,
pub problem: Option<Problem>,
pub detail: String,
}
impl std::fmt::Display for ProblemError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.detail)
}
}
impl std::error::Error for ProblemError {}
#[derive(Debug)]
pub enum RequestError {
Transport(Box<dyn std::error::Error + Send + Sync>),
Api(Box<ProblemError>),
Unreadable {
status: u16,
detail: String,
},
Unwritable {
detail: String,
},
}
impl RequestError {
pub fn transport<E>(cause: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Transport(Box::new(cause))
}
pub fn unwritable(cause: serde_json::Error) -> Self {
Self::Unwritable {
detail: cause.to_string(),
}
}
pub fn unreadable(status: u16, payload: &[u8], cause: &serde_json::Error) -> Self {
let seen = preview(payload);
let unread = format!("the API answered {status} with an unreadable body");
let detail = format!("{unread}: {cause} ({seen})");
Self::Unreadable { status, detail }
}
}
impl std::fmt::Display for RequestError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Transport(cause) => write!(formatter, "the API was not reached: {cause}"),
Self::Api(failure) => failure.fmt(formatter),
Self::Unreadable { detail, .. } => formatter.write_str(detail),
Self::Unwritable { detail } => formatter.write_str(detail),
}
}
}
impl std::error::Error for RequestError {}
pub fn problem_for(status: u16, payload: &[u8]) -> Option<ProblemError> {
if (LOWEST_SUCCESS..LOWEST_REDIRECTION).contains(&status) {
return None;
}
let problem: Option<Problem> = serde_json::from_slice(payload).ok();
let seen = preview(payload);
let kind = problem.as_ref().map(|problem| problem.id);
Some(ProblemError {
status,
kind,
problem,
detail: format!("the API answered {status}: {seen}"),
})
}
fn preview(payload: &[u8]) -> String {
let text = String::from_utf8_lossy(payload);
let mut kept = String::with_capacity(MAX_PREVIEW_BYTES);
for character in text.chars() {
if kept.len() + character.len_utf8() > MAX_PREVIEW_BYTES {
kept.push('…');
break;
}
kept.push(character);
}
kept
}