use std::fmt;
use serde_json::Value;
#[derive(Debug)]
pub enum PulseError {
Auth { path: String, body: Option<Value> },
NotFound { path: String, body: Option<Value> },
Validation { path: String, body: Option<Value> },
RateLimit {
path: String,
body: Option<Value>,
retry_after_seconds: Option<u32>,
},
Api {
status: u16,
path: String,
body: Option<Value>,
},
Transport(reqwest::Error),
Json(serde_json::Error),
NoToken { path: String },
InvalidConfig(String),
}
impl PulseError {
pub fn is_auth_error(&self) -> bool {
matches!(self, PulseError::Auth { .. } | PulseError::NoToken { .. })
}
pub fn is_not_found(&self) -> bool {
matches!(self, PulseError::NotFound { .. })
}
pub fn is_validation_error(&self) -> bool {
matches!(self, PulseError::Validation { .. })
}
pub fn is_rate_limited(&self) -> bool {
matches!(self, PulseError::RateLimit { .. })
}
pub fn status_code(&self) -> Option<u16> {
match self {
PulseError::Auth { .. } | PulseError::NoToken { .. } => Some(401),
PulseError::NotFound { .. } => Some(404),
PulseError::Validation { .. } => Some(400),
PulseError::RateLimit { .. } => Some(429),
PulseError::Api { status, .. } => Some(*status),
PulseError::Transport(_) | PulseError::Json(_) | PulseError::InvalidConfig(_) => None,
}
}
pub fn body(&self) -> Option<&Value> {
match self {
PulseError::Auth { body, .. }
| PulseError::NotFound { body, .. }
| PulseError::Validation { body, .. }
| PulseError::RateLimit { body, .. }
| PulseError::Api { body, .. } => body.as_ref(),
_ => None,
}
}
pub fn path(&self) -> Option<&str> {
match self {
PulseError::Auth { path, .. }
| PulseError::NotFound { path, .. }
| PulseError::Validation { path, .. }
| PulseError::RateLimit { path, .. }
| PulseError::Api { path, .. }
| PulseError::NoToken { path } => Some(path),
_ => None,
}
}
}
impl fmt::Display for PulseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let summary = match self {
PulseError::Auth { path, body } => format_http(401, path, body.as_ref()),
PulseError::NotFound { path, body } => format_http(404, path, body.as_ref()),
PulseError::Validation { path, body } => format_http(400, path, body.as_ref()),
PulseError::RateLimit { path, body, .. } => format_http(429, path, body.as_ref()),
PulseError::Api { status, path, body } => format_http(*status, path, body.as_ref()),
PulseError::Transport(e) => return write!(f, "pulse: HTTP transport failure — {e}"),
PulseError::Json(e) => return write!(f, "pulse: JSON encode/decode failure — {e}"),
PulseError::NoToken { path } => {
return write!(
f,
"pulse: no token set for {path} — call client.auth().login(...).await first \
or pass .token(...) to the builder"
);
}
PulseError::InvalidConfig(msg) => return write!(f, "pulse: invalid config — {msg}"),
};
write!(f, "{summary}")
}
}
fn format_http(status: u16, path: &str, body: Option<&Value>) -> String {
let mut msg = format!("pulse: HTTP {status} from {path}");
if let Some(v) = body {
if let Some(err) = v
.get("error")
.and_then(Value::as_str)
.or_else(|| v.get("errorMessage").and_then(Value::as_str))
.or_else(|| v.get("message").and_then(Value::as_str))
{
msg.push_str(" — ");
msg.push_str(err);
}
}
msg
}
impl std::error::Error for PulseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
PulseError::Transport(e) => Some(e),
PulseError::Json(e) => Some(e),
_ => None,
}
}
}
impl From<reqwest::Error> for PulseError {
fn from(e: reqwest::Error) -> Self {
PulseError::Transport(e)
}
}
impl From<serde_json::Error> for PulseError {
fn from(e: serde_json::Error) -> Self {
PulseError::Json(e)
}
}