rust-ynab 0.4.1

A Rust client for the YNAB API
Documentation
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
pub(crate) struct ErrorResponse {
    pub(crate) error: ApiError,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ApiError {
    pub id: String,
    pub name: String,
    pub detail: String,
}

impl std::fmt::Display for ApiError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "({}) {} - {}", self.id, self.name, self.detail)
    }
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("bad request: {0}")]
    BadRequest(ApiError),
    #[error("internal server error: {0}")]
    InternalServerError(ApiError),
    #[error("unauthorized: {0}")]
    Unauthorized(ApiError),
    #[error("rate limited: {0}")]
    RateLimited(ApiError),
    #[error("not found: {0}")]
    NotFound(ApiError),
    #[error("forbidden: {0}")]
    Forbidden(ApiError),
    #[error("conflict: {0}")]
    Conflict(ApiError),
    #[error("service unavailable: {0}")]
    ServiceUnavailable(ApiError),
    #[error("unknown error: {0}")]
    UnknownError(ApiError),
    #[error(transparent)]
    Reqwest(#[from] reqwest::Error),
    #[error(transparent)]
    ParseError(#[from] url::ParseError),
    #[error("invalid rate limit configuration: {0}")]
    InvalidRateLimit(String),
}

impl Error {
    pub fn new_api_error(status: reqwest::StatusCode, api_error: ApiError) -> Self {
        match status {
            reqwest::StatusCode::BAD_REQUEST => Error::BadRequest(api_error),
            reqwest::StatusCode::INTERNAL_SERVER_ERROR => Error::InternalServerError(api_error),
            reqwest::StatusCode::UNAUTHORIZED => Error::Unauthorized(api_error),
            reqwest::StatusCode::TOO_MANY_REQUESTS => Error::RateLimited(api_error),
            reqwest::StatusCode::NOT_FOUND => Error::NotFound(api_error),
            reqwest::StatusCode::FORBIDDEN => Error::Forbidden(api_error),
            reqwest::StatusCode::CONFLICT => Error::Conflict(api_error),
            reqwest::StatusCode::SERVICE_UNAVAILABLE => Error::ServiceUnavailable(api_error),
            _ => Error::UnknownError(api_error),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use reqwest::StatusCode;

    fn api_error() -> ApiError {
        ApiError {
            id: "test".into(),
            name: "test_error".into(),
            detail: "something went
  wrong"
                .into(),
        }
    }

    #[test]
    fn maps_status_codes_to_error_variants() {
        let cases: &[(StatusCode, fn(&Error) -> bool)] = &[
            (StatusCode::BAD_REQUEST, |e| {
                matches!(e, Error::BadRequest(_))
            }),
            (StatusCode::UNAUTHORIZED, |e| {
                matches!(e, Error::Unauthorized(_))
            }),
            (StatusCode::FORBIDDEN, |e| matches!(e, Error::Forbidden(_))),
            (StatusCode::NOT_FOUND, |e| matches!(e, Error::NotFound(_))),
            (StatusCode::CONFLICT, |e| matches!(e, Error::Conflict(_))),
            (StatusCode::TOO_MANY_REQUESTS, |e| {
                matches!(e, Error::RateLimited(_))
            }),
            (StatusCode::INTERNAL_SERVER_ERROR, |e| {
                matches!(e, Error::InternalServerError(_))
            }),
            (StatusCode::SERVICE_UNAVAILABLE, |e| {
                matches!(e, Error::ServiceUnavailable(_))
            }),
            (StatusCode::IM_A_TEAPOT, |e| {
                matches!(e, Error::UnknownError(_))
            }),
        ];

        for (status, check) in cases {
            let err = Error::new_api_error(*status, api_error());
            assert!(check(&err), "wrong variant for status {status}");
        }
    }
}