use reqwest::StatusCode;
use std::fmt;
use thiserror::Error as ThisError;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ApiError {
status: StatusCode,
code: Option<i64>,
kind: Option<String>,
message: String,
body: String,
}
impl ApiError {
pub(crate) fn from_response(status: StatusCode, body: String) -> Self {
match serde_json::from_str::<crate::wire::WireApiError>(&body) {
Ok(payload) => Self {
status,
code: Some(payload.code),
kind: Some(payload.error),
message: payload.message,
body,
},
Err(_) => Self {
status,
code: None,
kind: None,
message: "unparseable API error response".to_string(),
body,
},
}
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn code(&self) -> Option<i64> {
self.code
}
pub fn kind(&self) -> Option<&str> {
self.kind.as_deref()
}
pub fn message(&self) -> &str {
&self.message
}
pub fn body(&self) -> &str {
&self.body
}
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind() {
Some(kind) => write!(f, "{} {}: {}", self.status, kind, self.message),
None => write!(f, "{}: {}", self.status, self.message),
}
}
}
#[derive(Debug, ThisError)]
pub enum Error {
#[error("api key cannot be empty")]
InvalidApiKey,
#[error("base URL is invalid: {0}")]
InvalidBaseUrl(#[from] url::ParseError),
#[error("at least one recipient is required")]
MissingRecipients,
#[error("subject cannot be empty")]
InvalidSubject,
#[error("body cannot be empty")]
InvalidBody,
#[error("template id cannot be empty")]
InvalidTemplateId,
#[error("display name cannot be empty")]
InvalidDisplayName,
#[error("header name cannot be empty")]
InvalidHeaderName,
#[error("header value cannot be empty")]
InvalidHeaderValue,
#[error("email address is invalid: {reason} ({value})")]
InvalidEmailAddress { reason: &'static str, value: String },
#[error("template data must serialize into a JSON object")]
TemplateDataMustBeObject,
#[error("failed to serialize template data: {0}")]
TemplateDataSerialization(serde_json::Error),
#[error("http request failed: {0}")]
Transport(#[from] reqwest::Error),
#[error("plunk API returned {0}")]
Api(ApiError),
#[error("unexpected API response: {0}")]
UnexpectedResponse(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_error_preserves_raw_body_when_json_is_unparseable() {
let error =
ApiError::from_response(StatusCode::BAD_GATEWAY, "<html>bad gateway</html>".into());
assert_eq!(error.status(), StatusCode::BAD_GATEWAY);
assert_eq!(error.code(), None);
assert_eq!(error.body(), "<html>bad gateway</html>");
}
}