plunk-rs 0.1.1

Async Rust client for the Plunk transactional email API
Documentation
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>");
    }
}