#![allow(missing_docs)]
use std::collections::HashMap;
use serde::Deserialize;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("HTTP 401: {message}")]
Auth {
message: String,
body: Option<serde_json::Value>,
},
#[error("HTTP 403: {message}")]
Forbidden {
message: String,
body: Option<serde_json::Value>,
},
#[error("HTTP 409: {message}")]
Conflict {
message: String,
body: Option<serde_json::Value>,
},
#[error("HTTP 422: {message}")]
Validation {
message: String,
errors: HashMap<String, Vec<String>>,
body: Option<serde_json::Value>,
},
#[error("HTTP 429: {message}")]
RateLimit {
message: String,
retry_after: Option<u64>,
limit: Option<u64>,
remaining: Option<u64>,
body: Option<serde_json::Value>,
},
#[error("HTTP {status}: {message}")]
Server {
status: u16,
message: String,
body: Option<serde_json::Value>,
},
#[error("HTTP {status}: {message}")]
Api {
status: u16,
message: String,
body: Option<serde_json::Value>,
},
#[error("network error: {0}")]
Network(#[from] reqwest::Error),
#[error("serialization error: {0}")]
Serialize(serde_json::Error),
#[error("deserialization error: {0}")]
Deserialize(serde_json::Error),
}
impl Error {
#[must_use]
pub fn is_conflict(&self) -> bool {
matches!(self, Error::Conflict { .. })
}
}
#[derive(Deserialize)]
struct ErrorBody {
error: Option<String>,
message: Option<String>,
errors: Option<HashMap<String, Vec<String>>>,
}
pub(crate) fn from_response(
status: u16,
body_text: &str,
headers: &reqwest::header::HeaderMap,
) -> Error {
let body_value: Option<serde_json::Value> = serde_json::from_str(body_text).ok();
let parsed: Option<ErrorBody> = serde_json::from_str(body_text).ok();
let message = parsed
.as_ref()
.and_then(|e| e.error.clone().or_else(|| e.message.clone()))
.unwrap_or_else(|| format!("Request failed ({status})"));
match status {
401 => Error::Auth {
message,
body: body_value,
},
403 => Error::Forbidden {
message,
body: body_value,
},
409 => Error::Conflict {
message,
body: body_value,
},
422 => Error::Validation {
message,
errors: parsed.and_then(|e| e.errors).unwrap_or_default(),
body: body_value,
},
429 => Error::RateLimit {
message,
retry_after: parse_int_header(headers, "retry-after"),
limit: parse_int_header(headers, "x-ratelimit-limit"),
remaining: parse_int_header(headers, "x-ratelimit-remaining"),
body: body_value,
},
s if s >= 500 => Error::Server {
status: s,
message,
body: body_value,
},
s => Error::Api {
status: s,
message,
body: body_value,
},
}
}
fn parse_int_header(headers: &reqwest::header::HeaderMap, name: &str) -> Option<u64> {
headers.get(name)?.to_str().ok()?.parse().ok()
}
pub type Result<T> = std::result::Result<T, Error>;