use serde::{Deserialize, Serialize};
pub type Result<T> = std::result::Result<T, ReroutError>;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ApiErrorDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
#[derive(Debug, thiserror::Error)]
pub enum ReroutError {
#[error("Rerout API error ({code}, HTTP {status}): {message}")]
Api {
code: String,
status: u16,
message: String,
extra: Box<ApiErrorDetails>,
},
#[error("Rerout network error: {message}")]
Network {
message: String,
},
#[error("Rerout request timed out: {message}")]
Timeout {
message: String,
},
#[error("Rerout returned an unexpected response (HTTP {status}): {message}")]
Unexpected {
status: u16,
message: String,
},
#[error("Rerout response decode error: {message}")]
Decode {
message: String,
},
#[error("Rerout configuration error ({code}): {message}")]
Config {
code: String,
message: String,
},
}
impl ReroutError {
pub fn code(&self) -> &str {
match self {
ReroutError::Api { code, .. } => code,
ReroutError::Network { .. } => "network_error",
ReroutError::Timeout { .. } => "timeout",
ReroutError::Unexpected { .. } => "unexpected_response",
ReroutError::Decode { .. } => "decode_error",
ReroutError::Config { code, .. } => code,
}
}
pub fn status(&self) -> u16 {
match self {
ReroutError::Api { status, .. } | ReroutError::Unexpected { status, .. } => *status,
ReroutError::Network { .. }
| ReroutError::Timeout { .. }
| ReroutError::Decode { .. }
| ReroutError::Config { .. } => 0,
}
}
pub fn message(&self) -> String {
match self {
ReroutError::Api { message, .. }
| ReroutError::Network { message, .. }
| ReroutError::Timeout { message, .. }
| ReroutError::Unexpected { message, .. }
| ReroutError::Decode { message, .. }
| ReroutError::Config { message, .. } => message.clone(),
}
}
pub fn is_rate_limited(&self) -> bool {
self.status() == 429
}
pub fn is_server_error(&self) -> bool {
let s = self.status();
(500..600).contains(&s)
}
pub fn path(&self) -> Option<&str> {
match self {
ReroutError::Api { extra, .. } => extra.path.as_deref(),
_ => None,
}
}
pub fn timestamp(&self) -> Option<&str> {
match self {
ReroutError::Api { extra, .. } => extra.timestamp.as_deref(),
_ => None,
}
}
pub fn details(&self) -> Option<&serde_json::Value> {
match self {
ReroutError::Api { extra, .. } => extra.details.as_ref(),
_ => None,
}
}
}
pub(crate) fn synthetic_code_for_status(status: u16) -> &'static str {
match status {
401 => "unauthorized",
403 => "forbidden",
404 => "not_found",
429 => "rate_limited",
500..=599 => "server_error",
_ => "client_error",
}
}
pub(crate) fn build_api_error(status: u16, body: &str) -> ReroutError {
if body.is_empty() {
return ReroutError::Api {
code: synthetic_code_for_status(status).to_string(),
status,
message: format!("Rerout returned HTTP {status} with no body."),
extra: Box::default(),
};
}
match serde_json::from_str::<serde_json::Value>(body) {
Ok(serde_json::Value::Object(map)) => {
let code = map
.get("code")
.and_then(|v| v.as_str())
.map(str::to_string)
.unwrap_or_else(|| synthetic_code_for_status(status).to_string());
let message = map
.get("message")
.and_then(|v| v.as_str())
.map(str::to_string)
.unwrap_or_else(|| format!("Rerout returned HTTP {status}."));
let path = map.get("path").and_then(|v| v.as_str()).map(str::to_string);
let timestamp = map
.get("timestamp")
.and_then(|v| v.as_str())
.map(str::to_string);
let details = map.get("details").cloned();
ReroutError::Api {
code,
status,
message,
extra: Box::new(ApiErrorDetails {
path,
timestamp,
details,
}),
}
}
_ => ReroutError::Api {
code: synthetic_code_for_status(status).to_string(),
status,
message: format!("Rerout returned HTTP {status} (non-JSON body)."),
extra: Box::default(),
},
}
}