Skip to main content

rust_ynab/ynab/
errors.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Deserialize)]
4pub(crate) struct ErrorResponse {
5    pub(crate) error: ApiError,
6}
7
8#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
9pub struct ApiError {
10    pub id: String,
11    pub name: String,
12    pub detail: String,
13}
14
15impl std::fmt::Display for ApiError {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        write!(f, "({}) {} - {}", self.id, self.name, self.detail)
18    }
19}
20
21#[derive(Debug, thiserror::Error)]
22pub enum Error {
23    #[error("bad request: {0}")]
24    BadRequest(ApiError),
25    #[error("internal server error: {0}")]
26    InternalServerError(ApiError),
27    #[error("unauthorized: {0}")]
28    Unauthorized(ApiError),
29    #[error("rate limited: {0}")]
30    RateLimited(ApiError),
31    #[error("not found: {0}")]
32    NotFound(ApiError),
33    #[error("forbidden: {0}")]
34    Forbidden(ApiError),
35    #[error("conflict: {0}")]
36    Conflict(ApiError),
37    #[error("service unavailable: {0}")]
38    ServiceUnavailable(ApiError),
39    #[error("unknown error: {0}")]
40    UnknownError(ApiError),
41    #[error(transparent)]
42    Reqwest(#[from] reqwest::Error),
43    #[error(transparent)]
44    ParseError(#[from] url::ParseError),
45    #[error("invalid rate limit configuration: {0}")]
46    InvalidRateLimit(String),
47    #[error("OAuth Error: {0}")]
48    OAuth(String),
49}
50
51impl Error {
52    pub fn new_api_error(status: reqwest::StatusCode, api_error: ApiError) -> Self {
53        match status {
54            reqwest::StatusCode::BAD_REQUEST => Error::BadRequest(api_error),
55            reqwest::StatusCode::INTERNAL_SERVER_ERROR => Error::InternalServerError(api_error),
56            reqwest::StatusCode::UNAUTHORIZED => Error::Unauthorized(api_error),
57            reqwest::StatusCode::TOO_MANY_REQUESTS => Error::RateLimited(api_error),
58            reqwest::StatusCode::NOT_FOUND => Error::NotFound(api_error),
59            reqwest::StatusCode::FORBIDDEN => Error::Forbidden(api_error),
60            reqwest::StatusCode::CONFLICT => Error::Conflict(api_error),
61            reqwest::StatusCode::SERVICE_UNAVAILABLE => Error::ServiceUnavailable(api_error),
62            _ => Error::UnknownError(api_error),
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use reqwest::StatusCode;
71
72    fn api_error() -> ApiError {
73        ApiError {
74            id: "test".into(),
75            name: "test_error".into(),
76            detail: "something went
77  wrong"
78                .into(),
79        }
80    }
81
82    type StatusCase = (StatusCode, fn(&Error) -> bool);
83
84    #[test]
85    fn maps_status_codes_to_error_variants() {
86        let cases: &[StatusCase] = &[
87            (StatusCode::BAD_REQUEST, |e| {
88                matches!(e, Error::BadRequest(_))
89            }),
90            (StatusCode::UNAUTHORIZED, |e| {
91                matches!(e, Error::Unauthorized(_))
92            }),
93            (StatusCode::FORBIDDEN, |e| matches!(e, Error::Forbidden(_))),
94            (StatusCode::NOT_FOUND, |e| matches!(e, Error::NotFound(_))),
95            (StatusCode::CONFLICT, |e| matches!(e, Error::Conflict(_))),
96            (StatusCode::TOO_MANY_REQUESTS, |e| {
97                matches!(e, Error::RateLimited(_))
98            }),
99            (StatusCode::INTERNAL_SERVER_ERROR, |e| {
100                matches!(e, Error::InternalServerError(_))
101            }),
102            (StatusCode::SERVICE_UNAVAILABLE, |e| {
103                matches!(e, Error::ServiceUnavailable(_))
104            }),
105            (StatusCode::IM_A_TEAPOT, |e| {
106                matches!(e, Error::UnknownError(_))
107            }),
108        ];
109
110        for (status, check) in cases {
111            let err = Error::new_api_error(*status, api_error());
112            assert!(check(&err), "wrong variant for status {status}");
113        }
114    }
115}